diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05723ea36..bdfd6e017 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: strategy: matrix: goversion: - - 1.18 + - 1.19 steps: - name: Checkout repository diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 12800b906..6f2b41213 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: '1.18' + go-version: '1.19' - name: golangci-lint-libs uses: golangci/golangci-lint-action@v3 with: @@ -34,7 +34,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: '1.18' + go-version: '1.19' - name: golangci-lint-services uses: golangci/golangci-lint-action@v3 with: @@ -49,7 +49,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: '1.18' + go-version: '1.19' - name: golangci-lint-tools uses: golangci/golangci-lint-action@v3 with: @@ -64,7 +64,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: '1.18' + go-version: '1.19' - name: golangci-lint-cmd uses: golangci/golangci-lint-action@v3 with: @@ -79,7 +79,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: '1.18' + go-version: '1.19' - name: golangci-lint-main uses: golangci/golangci-lint-action@v3 with: diff --git a/.golangci.yaml b/.golangci.yaml index 534a69969..960208527 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,5 +1,5 @@ run: - go: "1.17" ## TODO update to 1.18 once all linters support it + go: "1.19" timeout: 3m linters-settings: diff --git a/Dockerfile b/Dockerfile index 0ae3bec81..68c6c5e57 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.18-alpine as builder +FROM golang:1.19-alpine as builder # Put certs in builder image. RUN apk update @@ -20,7 +20,8 @@ RUN cd main && go mod download && CGO_ENABLED=0 GOOS=linux go build \ -ldflags "-w -s -X main.version=${VERSION} -X main.buildTime=${BUILD_TIME} -X main.commit=${COMMIT}" \ -o bat-go main.go -FROM alpine:3.15 as base +# golang:1.19-alpine is based on alpine:3.18. +FROM alpine:3.18 as base # Put certs in artifact from builder. COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ diff --git a/README.md b/README.md index a5b5ddfe4..a4d37604c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # pass "go" and collect 200 BAT - ![CI](https://github.com/brave-intl/bat-go/workflows/CI/badge.svg) + ## Developer Setup 1. [Install Go 1.12](https://golang.org/doc/install) (NOTE: Go 1.10 and earlier will not work!) diff --git a/docker-compose.dev-refresh.yml b/docker-compose.dev-refresh.yml index 0936b79ba..ca5c07ea3 100644 --- a/docker-compose.dev-refresh.yml +++ b/docker-compose.dev-refresh.yml @@ -8,7 +8,7 @@ services: # every time a file changes. dev-refresh: container_name: grant-dev-refresh - image: golang:1.18 + image: golang:1.19 ports: - "3335:3333" - "6061:6061" @@ -39,7 +39,6 @@ services: - "DATABASE_MIGRATIONS_URL=file:///src/migrations" - "DATABASE_URL=postgres://grants:password@postgres/grants?sslmode=disable" - ENCRYPTION_KEY=MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0 - - FEATURE_MERCHANT=true - DONOR_WALLET_CARD_ID - DONOR_WALLET_PRIVATE_KEY - DONOR_WALLET_PUBLIC_KEY diff --git a/ecs-vars-dev.env b/ecs-vars-dev.env index 900dfb584..331071f80 100644 --- a/ecs-vars-dev.env +++ b/ecs-vars-dev.env @@ -8,7 +8,6 @@ DATABASE_MIGRATIONS_URL=file://migrations DEBUG=0 ENABLE_JOB_WORKERS=true ENABLE_LINKING_DRAINING=true -FEATURE_MERCHANT=true GRANT_SIGNATOR_PUBLIC_KEY=90f006eecd80f21bd26dc363155355c2b857cdd9d77b550db23bccaa2866c3c3 GRANT_WALLET_CARD_ID=9094c3f2-b3ae-438f-bd59-92aaad92de5c GRANT_WALLET_PUBLIC_KEY=37bb424aa56661058768d6d074163162d50c8f6f9ed64f2f491aac307f0acade diff --git a/libs/clients/bitflyer/client.go b/libs/clients/bitflyer/client.go index f1e0ef2f1..079246628 100644 --- a/libs/clients/bitflyer/client.go +++ b/libs/clients/bitflyer/client.go @@ -10,11 +10,8 @@ import ( "net/url" "os" "runtime/debug" - "strings" "time" - "github.com/prometheus/client_golang/prometheus" - "github.com/brave-intl/bat-go/libs/altcurrency" "github.com/brave-intl/bat-go/libs/clients" appctx "github.com/brave-intl/bat-go/libs/context" @@ -27,11 +24,6 @@ import ( ) var ( - bfBalanceGauge = prometheus.NewGauge(prometheus.GaugeOpts{ - Name: "bitflyer_account_balance", - Help: "A gauge of the current account balance in bitflyer", - }) - validSourceFrom = map[string]bool{ "tipping": true, "adrewards": true, @@ -39,57 +31,6 @@ var ( } ) -func init() { - prometheus.MustRegister(bfBalanceGauge) -} - -// WatchBitflyerBalance periodically checks bitflyer inventory balance for BAT -func WatchBitflyerBalance(ctx context.Context, duration time.Duration) error { - client, err := New() - if err != nil { - return fmt.Errorf("failed to create bitflyer client: %w", err) - } - - _, err = client.RefreshToken(ctx, TokenPayloadFromCtx(ctx)) - if err != nil { - return fmt.Errorf("failed to get bitflyer access token: %w", err) - } - - for { - select { - case <-ctx.Done(): - return nil - case <-time.After(duration): - go func() { - result, err := client.FetchBalance(ctx) - if err != nil { - logging.FromContext(ctx).Error().Err(err). - Msg("bitflyer client error") - } else { - found := false - for _, inv := range result.Inventory { - if strings.ToLower(inv.CurrencyCode) == "bat" { - found = true - if inv.Amount.LessThan(decimal.NewFromFloat(1.0)) { - logging.FromContext(ctx).Error().Err(errors.New("account is empty")). - Msg("bitflyer account error") - } else { - tmp, _ := inv.Amount.Float64() - bfBalanceGauge.Set(tmp) - } - break - } - } - if !found { - logging.FromContext(ctx).Error().Err(errors.New("currency code BAT not found in response")). - Msg("bitflyer response error") - } - } - }() - } - } -} - // Quote returns a quote of BAT prices type Quote struct { ProductCode string `json:"product_code"` diff --git a/libs/clients/bitflyer/instrumented_client.go b/libs/clients/bitflyer/instrumented_client.go index 07c9fabcd..f3a9d1674 100755 --- a/libs/clients/bitflyer/instrumented_client.go +++ b/libs/clients/bitflyer/instrumented_client.go @@ -1,9 +1,9 @@ +package bitflyer + // Code generated by gowrap. DO NOT EDIT. // template: ../../../.prom-gowrap.tmpl // gowrap: http://github.com/hexdigest/gowrap -package bitflyer - //go:generate gowrap gen -p github.com/brave-intl/bat-go/libs/clients/-i Client -t ../../../.prom-gowrap.tmpl -o instrumented_client.go -l "" import ( diff --git a/libs/clients/cbr/instrumented_client.go b/libs/clients/cbr/instrumented_client.go index 307918cf4..c1ccdf616 100755 --- a/libs/clients/cbr/instrumented_client.go +++ b/libs/clients/cbr/instrumented_client.go @@ -1,9 +1,9 @@ +package cbr + // Code generated by gowrap. DO NOT EDIT. // template: ../../../.prom-gowrap.tmpl // gowrap: http://github.com/hexdigest/gowrap -package cbr - //go:generate gowrap gen -p github.com/brave-intl/bat-go/libs/clients/-i Client -t ../../../.prom-gowrap.tmpl -o instrumented_client.go -l "" import ( diff --git a/libs/clients/coingecko/instrumented_client.go b/libs/clients/coingecko/instrumented_client.go index 3035c0f07..79ed58365 100755 --- a/libs/clients/coingecko/instrumented_client.go +++ b/libs/clients/coingecko/instrumented_client.go @@ -1,9 +1,9 @@ +package coingecko + // Code generated by gowrap. DO NOT EDIT. // template: ../../../.prom-gowrap.tmpl // gowrap: http://github.com/hexdigest/gowrap -package coingecko - //go:generate gowrap gen -p github.com/brave-intl/bat-go/libs/clients/-i Client -t ../../../.prom-gowrap.tmpl -o instrumented_client.go -l "" import ( diff --git a/libs/clients/gemini/client.go b/libs/clients/gemini/client.go index 10f217030..95dcd182f 100644 --- a/libs/clients/gemini/client.go +++ b/libs/clients/gemini/client.go @@ -20,7 +20,6 @@ import ( "github.com/brave-intl/bat-go/libs/cryptography" "github.com/brave-intl/bat-go/libs/custodian" errorutils "github.com/brave-intl/bat-go/libs/errors" - "github.com/brave-intl/bat-go/libs/logging" "github.com/google/go-querystring/query" "github.com/prometheus/client_golang/prometheus" "github.com/shengdoushi/base58" @@ -41,11 +40,6 @@ func isIssueCountryEnabled() bool { } var ( - balanceGauge = prometheus.NewGauge(prometheus.GaugeOpts{ - Name: "gemini_account_balance", - Help: "A gauge of the current account balance in gemini", - }) - countGeminiWalletAccountValidation = prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "count_gemini_wallet_account_validation", @@ -74,72 +68,10 @@ var ( var ErrNoAcceptedDocumentType = errors.New("no accepted document type") func init() { - prometheus.MustRegister(balanceGauge) prometheus.MustRegister(countGeminiWalletAccountValidation) prometheus.MustRegister(countGeminiDocumentTypeByIssuingCountry) } -// WatchGeminiBalance - when called reports the balance to prometheus -func WatchGeminiBalance(ctx context.Context) error { - logger := logging.Logger(ctx, "WatchGeminiBalance") - // create a new gemini client - client, err := New() - if err != nil { - logger.Error().Err(err).Msg("failed to get gemini client") - return fmt.Errorf("failed to get gemini client: %w", err) - } - - // get api secret from context - apiSecret, err := appctx.GetStringFromContext(ctx, appctx.GeminiAPISecretCTXKey) - if err != nil { - logger.Error().Err(err).Msg("failed to get gemini api secret") - return fmt.Errorf("failed to get gemini api secret: %w", err) - } - apiKey, err := appctx.GetStringFromContext(ctx, appctx.GeminiAPIKeyCTXKey) - if err != nil { - logger.Error().Err(err).Msg("failed to get gemini api key") - return fmt.Errorf("failed to get gemini api key: %w", err) - } - //create a new hmac hasher - signer := cryptography.NewHMACHasher([]byte(apiSecret)) - for { - select { - case <-ctx.Done(): - return nil - // check every 10 min - case <-time.After(2 * 60 * time.Second): - // create the gemini payload - payload, err := json.Marshal(NewBalancesPayload(nil)) - if err != nil { - logger.Error().Err(err).Msg("failed to create gemini balance payload") - // okay to error, retry in 10 min - continue - } - - go func() { - defer func() { - if r := recover(); r != nil { - logger.Error().Str("panic", fmt.Sprintf("%+v", r)).Msg("failed to fetch gemini balance, panic") - } - }() - result, err := client.FetchBalances(ctx, apiKey, signer, string(payload)) - if err != nil { - logger.Error().Err(err).Msg("failed to fetch gemini balance") - } else { - // dont care about float downsampling from decimal errs - if result == nil || len(*result) < 1 { - logger.Error().Msg("gemini result is empty") - } else { - b := *result - available, _ := b[0].Available.Float64() - balanceGauge.Set(available) - } - } - }() - } - } -} - // PrivateRequestSequence handles the ability to sign a request multiple times type PrivateRequestSequence struct { // the baseline object, corresponds to the signature in the first item diff --git a/libs/clients/gemini/instrumented_client.go b/libs/clients/gemini/instrumented_client.go index 88d3578d7..10a954f92 100755 --- a/libs/clients/gemini/instrumented_client.go +++ b/libs/clients/gemini/instrumented_client.go @@ -1,9 +1,9 @@ +package gemini + // Code generated by gowrap. DO NOT EDIT. // template: ../../../.prom-gowrap.tmpl // gowrap: http://github.com/hexdigest/gowrap -package gemini - //go:generate gowrap gen -p github.com/brave-intl/bat-go/libs/clients/-i Client -t ../../../.prom-gowrap.tmpl -o instrumented_client.go -l "" import ( diff --git a/libs/clients/radom/instrumented.go b/libs/clients/radom/instrumented.go index d7ab2f1a5..ae1cd4c35 100644 --- a/libs/clients/radom/instrumented.go +++ b/libs/clients/radom/instrumented.go @@ -5,7 +5,6 @@ import ( "time" "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" ) type InstrumentedClient struct { @@ -14,19 +13,23 @@ type InstrumentedClient struct { vec *prometheus.SummaryVec } -// newInstrucmentedClient returns an instance of the Client decorated with prometheus summary metric. -func newInstrucmentedClient(name string, cl *Client) *InstrumentedClient { +// newInstrumentedClient returns an instance of the Client decorated with prometheus summary metric. +// This function panics if it cannot register the metric. +func newInstrumentedClient(name string, cl *Client) *InstrumentedClient { + v := prometheus.NewSummaryVec(prometheus.SummaryOpts{ + Name: "radom_client_duration_seconds", + Help: "client runtime duration and result", + MaxAge: time.Minute, + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }, + []string{"instance_name", "method", "result"}, + ) + prometheus.MustRegister(v) + result := &InstrumentedClient{ name: name, cl: cl, - vec: promauto.NewSummaryVec(prometheus.SummaryOpts{ - Name: "client_duration_seconds", - Help: "client runtime duration and result", - MaxAge: time.Minute, - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, - }, - []string{"instance_name", "method", "result"}, - ), + vec: v, } return result diff --git a/libs/clients/radom/radom.go b/libs/clients/radom/radom.go index d2e8e9c99..0efe49545 100644 --- a/libs/clients/radom/radom.go +++ b/libs/clients/radom/radom.go @@ -150,7 +150,7 @@ func NewInstrumented(srvURL, secret, proxyAddr string) (*InstrumentedClient, err return nil, err } - return newInstrucmentedClient("radom_client", cl), nil + return newInstrumentedClient("radom_client", cl), nil } func newClient(srvURL, secret, proxyAddr string) (*Client, error) { diff --git a/libs/clients/ratios/instrumented_client.go b/libs/clients/ratios/instrumented_client.go index 686e0b1af..6b1e06ebf 100755 --- a/libs/clients/ratios/instrumented_client.go +++ b/libs/clients/ratios/instrumented_client.go @@ -1,9 +1,9 @@ +package ratios + // Code generated by gowrap. DO NOT EDIT. // template: ../../../.prom-gowrap.tmpl // gowrap: http://github.com/hexdigest/gowrap -package ratios - //go:generate gowrap gen -p github.com/brave-intl/bat-go/libs/clients/-i Client -t ../../../.prom-gowrap.tmpl -o instrumented_client.go -l "" import ( diff --git a/libs/clients/reputation/instrumented_client.go b/libs/clients/reputation/instrumented_client.go index 6b02b3f0e..e1cceea29 100755 --- a/libs/clients/reputation/instrumented_client.go +++ b/libs/clients/reputation/instrumented_client.go @@ -1,9 +1,9 @@ +package reputation + // Code generated by gowrap. DO NOT EDIT. // template: ../../../.prom-gowrap.tmpl // gowrap: http://github.com/hexdigest/gowrap -package reputation - //go:generate gowrap gen -p github.com/brave-intl/bat-go/libs/clients/-i Client -t ../../../.prom-gowrap.tmpl -o instrumented_client.go -l "" import ( diff --git a/libs/clients/stripe/instrumented_client.go b/libs/clients/stripe/instrumented_client.go index 2383ba792..bfc3224a5 100644 --- a/libs/clients/stripe/instrumented_client.go +++ b/libs/clients/stripe/instrumented_client.go @@ -1,9 +1,9 @@ +package stripe + // Code generated by gowrap. DO NOT EDIT. // template: ../../../.prom-gowrap.tmpl // gowrap: http://github.com/hexdigest/gowrap -package stripe - //go:generate gowrap gen -p github.com/brave-intl/bat-go/libs/clients/-i Client -t ../../../.prom-gowrap.tmpl -o instrumented_client.go -l "" import ( diff --git a/libs/ptr/ptr.go b/libs/ptr/ptr.go index d35642274..6c1667b17 100644 --- a/libs/ptr/ptr.go +++ b/libs/ptr/ptr.go @@ -33,3 +33,7 @@ func StringOr(s *string, or string) string { func FromTime(t time.Time) *time.Time { return &t } + +func To[T any](v T) *T { + return &v +} diff --git a/services/grant/cmd/grant.go b/services/grant/cmd/grant.go index a175a4b9a..56cc51470 100644 --- a/services/grant/cmd/grant.go +++ b/services/grant/cmd/grant.go @@ -12,7 +12,7 @@ import ( "time" "github.com/asaskevich/govalidator" - sentry "github.com/getsentry/sentry-go" + "github.com/getsentry/sentry-go" "github.com/go-chi/chi" chiware "github.com/go-chi/chi/middleware" "github.com/rs/zerolog" @@ -23,8 +23,6 @@ import ( "github.com/brave-intl/bat-go/cmd" cmdutils "github.com/brave-intl/bat-go/cmd" - "github.com/brave-intl/bat-go/libs/clients/bitflyer" - "github.com/brave-intl/bat-go/libs/clients/gemini" "github.com/brave-intl/bat-go/libs/clients/reputation" appctx "github.com/brave-intl/bat-go/libs/context" "github.com/brave-intl/bat-go/libs/handlers" @@ -507,27 +505,6 @@ func setupRouter(ctx context.Context, logger *zerolog.Logger) (context.Context, r.Mount("/v1/webhooks", skus.WebhookRouter(skusService)) r.Mount("/v1/votes", skus.VoteRouter(skusService, middleware.InstrumentHandler)) - if os.Getenv("FEATURE_MERCHANT") != "" { - skusDB, err := skus.NewPostgres( - skuOrderRepo, - skuOrderItemRepo, - skuOrderPayHistRepo, - skuIssuerRepo, - "", true, "merch_skus_db", - ) - if err != nil { - sentry.CaptureException(err) - logger.Panic().Err(err).Msg("Must be able to init postgres connection to start") - } - - skusService, err := skus.InitService(ctx, skusDB, walletService, skuOrderRepo, skuIssuerRepo) - if err != nil { - sentry.CaptureException(err) - logger.Panic().Err(err).Msg("SKUs service initialization failed") - } - r.Mount("/v1/merchants", skus.MerchantRouter(skusService)) - } - // add profiling flag to enable profiling routes if os.Getenv("PPROF_ENABLED") != "" { // pprof attaches routes to default serve mux @@ -705,21 +682,6 @@ func GrantServer( } } } - if viper.GetString("environment") != "local" && - viper.GetString("environment") != "development" { - // run gemini balance watch so we have balance info in prometheus - go func() { - // no need to panic here, log the error and move on with serving - if err := gemini.WatchGeminiBalance(ctx); err != nil { - logger.Error().Err(err).Msg("error launching gemini balance watch") - } - }() - go func() { - if err := bitflyer.WatchBitflyerBalance(ctx, 2*time.Minute); err != nil { - logger.Error().Err(err).Msg("error launching bitflyer balance watch") - } - }() - } go func() { err := http.ListenAndServe(":9090", middleware.Metrics()) diff --git a/services/grant/instrumented_datastore.go b/services/grant/instrumented_datastore.go index 6f692f7d1..cdc569074 100755 --- a/services/grant/instrumented_datastore.go +++ b/services/grant/instrumented_datastore.go @@ -1,9 +1,9 @@ +package grant + // Code generated by gowrap. DO NOT EDIT. // template: ../../.prom-gowrap.tmpl // gowrap: http://github.com/hexdigest/gowrap -package grant - //go:generate gowrap gen -p github.com/brave-intl/bat-go/services/grant -i Datastore -t ../../.prom-gowrap.tmpl -o instrumented_datastore.go -l "" import ( diff --git a/services/grant/instrumented_read_only_datastore.go b/services/grant/instrumented_read_only_datastore.go index 0ba4f01e5..f6020e490 100644 --- a/services/grant/instrumented_read_only_datastore.go +++ b/services/grant/instrumented_read_only_datastore.go @@ -1,9 +1,9 @@ +package grant + // Code generated by gowrap. DO NOT EDIT. // template: ../../.prom-gowrap.tmpl // gowrap: http://github.com/hexdigest/gowrap -package grant - //go:generate gowrap gen -p github.com/brave-intl/bat-go/services/grant -i ReadOnlyDatastore -t ../../.prom-gowrap.tmpl -o instrumented_read_only_datastore.go -l "" import ( diff --git a/services/grant/service.go b/services/grant/service.go index fe31419ac..6b01cb661 100644 --- a/services/grant/service.go +++ b/services/grant/service.go @@ -2,42 +2,20 @@ package grant import ( "context" - "crypto/ed25519" - "encoding/hex" - "errors" - "os" - "github.com/brave-intl/bat-go/libs/altcurrency" - errorutils "github.com/brave-intl/bat-go/libs/errors" - "github.com/brave-intl/bat-go/libs/httpsignature" srv "github.com/brave-intl/bat-go/libs/service" - walletutils "github.com/brave-intl/bat-go/libs/wallet" - "github.com/brave-intl/bat-go/libs/wallet/provider/uphold" "github.com/brave-intl/bat-go/services/promotion" "github.com/brave-intl/bat-go/services/wallet" - sentry "github.com/getsentry/sentry-go" - "github.com/prometheus/client_golang/prometheus" - "github.com/shopspring/decimal" ) -const localEnv = "local" - -var ( - grantWalletPublicKeyHex = os.Getenv("GRANT_WALLET_PUBLIC_KEY") - grantWalletPrivateKeyHex = os.Getenv("GRANT_WALLET_PRIVATE_KEY") - grantWalletCardID = os.Getenv("GRANT_WALLET_CARD_ID") - grantWallet *uphold.Wallet -) - -// Service contains datastore as well as prometheus metrics +// Service contains datastore type Service struct { - baseCtx context.Context - Datastore Datastore - RoDatastore ReadOnlyDatastore - wallet *wallet.Service - promotion *promotion.Service - grantWalletBalanceDesc *prometheus.Desc - jobs []srv.Job + baseCtx context.Context + Datastore Datastore + RoDatastore ReadOnlyDatastore + wallet *wallet.Service + promotion *promotion.Service + jobs []srv.Job } // Jobs - Implement srv.JobService interface @@ -59,51 +37,10 @@ func InitService( RoDatastore: roDatastore, wallet: walletService, promotion: promotionService, - grantWalletBalanceDesc: prometheus.NewDesc( - "grant_wallet_balance", - "A gauge of the grant wallet remaining balance.", - []string{}, - prometheus.Labels{}, - ), } // setup runnable jobs gs.jobs = []srv.Job{} - - if len(grantWalletCardID) > 0 { - var info walletutils.Info - info.Provider = "uphold" - info.ProviderID = grantWalletCardID - { - tmp := altcurrency.BAT - info.AltCurrency = &tmp - } - - var pubKey httpsignature.Ed25519PubKey - var privKey ed25519.PrivateKey - var err error - - pubKey, err = hex.DecodeString(grantWalletPublicKeyHex) - if err != nil { - return nil, errorutils.Wrap(err, "grantWalletPublicKeyHex is invalid") - } - privKey, err = hex.DecodeString(grantWalletPrivateKeyHex) - if err != nil { - return nil, errorutils.Wrap(err, "grantWalletPrivateKeyHex is invalid") - } - - grantWallet, err = uphold.New(ctx, info, privKey, pubKey) - if err != nil { - return nil, err - } - } else if os.Getenv("ENV") != localEnv { - return nil, errors.New("GRANT_WALLET_CARD_ID must be set in production") - } - - if datastore != nil { - prometheus.MustRegister(gs) - } - return gs, nil } @@ -114,32 +51,3 @@ func (s *Service) ReadableDatastore() ReadOnlyDatastore { } return s.Datastore } - -// Describe returns all descriptions of the collector. -// We implement this and the Collect function to fulfill the prometheus.Collector interface -func (s *Service) Describe(ch chan<- *prometheus.Desc) { - ch <- s.grantWalletBalanceDesc -} - -// Collect returns the current state of all metrics of the collector. -// We implement this and the Describe function to fulfill the prometheus.Collector interface -func (s *Service) Collect(ch chan<- prometheus.Metric) { - balance, err := grantWallet.GetBalance(s.baseCtx, true) - if err != nil { - sentry.CaptureException(err) - balance = grantWallet.LastBalance - if balance == nil { - balance = &walletutils.Balance{ - SpendableProbi: decimal.Zero, - } - } - } - - spendable, _ := grantWallet.GetWalletInfo().AltCurrency.FromProbi(balance.SpendableProbi).Float64() - - ch <- prometheus.MustNewConstMetric( - s.grantWalletBalanceDesc, - prometheus.GaugeValue, - spendable, - ) -} diff --git a/services/promotion/controllers.go b/services/promotion/controllers.go index 8caf43362..cf0405e99 100644 --- a/services/promotion/controllers.go +++ b/services/promotion/controllers.go @@ -62,10 +62,7 @@ func Router(service *Service, vbatExpires time.Time) chi.Router { r.Method("POST", "/reportclobberedclaims", middleware.InstrumentHandler("ReportClobberedClaims", PostReportClobberedClaims(service, 1))) r.Method("POST", "/{promotionId}", middleware.HTTPSignedOnly(service)(middleware.InstrumentHandler("ClaimPromotion", ClaimPromotion(service)))) r.Method("GET", "/{promotionId}/claims/{claimId}", middleware.InstrumentHandler("GetClaim", GetClaim(service))) - r.Method("GET", "/drain/{drainId}", middleware.InstrumentHandler("GetDrainPoll", GetDrainPoll(service))) r.Method("POST", "/report-bap", middleware.HTTPSignedOnly(service)(middleware.InstrumentHandler("PostReportBAPEvent", PostReportBAPEvent(service)))) - r.Method("GET", "/custodian-drain-status/{paymentId}", middleware.SimpleTokenAuthorizedOnly(middleware.InstrumentHandler("GetCustodianDrainInfo", GetCustodianDrainInfo(service)))) - r.Method("PATCH", "/drain-jobs/wallets/{walletId}/erred", middleware.SimpleTokenAuthorizedOnly(middleware.InstrumentHandler("PatchDrainJobErred", PatchDrainJobErred(service)))) return r } @@ -300,51 +297,6 @@ func ClaimPromotion(service *Service) handlers.AppHandler { }) } -// DrainPollResponse - structure for a drain poll response -type DrainPollResponse struct { - ID *uuid.UUID `json:"drainId"` - Status string `json:"status"` -} - -// GetDrainPoll is the handler for checking on a particular claim's status -func GetDrainPoll(service *Service) handlers.AppHandler { - return handlers.AppHandler(func(w http.ResponseWriter, r *http.Request) *handlers.AppError { - var drainID = new(inputs.ID) - if err := inputs.DecodeAndValidateString(context.Background(), drainID, chi.URLParam(r, "drainId")); err != nil { - return handlers.ValidationError( - "Error validating request url parameter", - map[string]interface{}{ - "drainId": err.Error(), - }, - ) - } - - var resp = &DrainPollResponse{} - - drainPoll, err := service.Datastore.GetDrainPoll(drainID.UUID()) - if err != nil { - return handlers.WrapError(err, "Error getting drain poll by id", http.StatusBadRequest) - } - - if drainPoll == nil { - return &handlers.AppError{ - Message: "Drain Job does not exist", - Code: http.StatusNotFound, - Data: map[string]interface{}{}, - } - } - - resp.ID = drainPoll.ID - resp.Status = drainPoll.Status - - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(resp); err != nil { - panic(err) - } - return nil - }) -} - // GetClaimResponse includes signed credentials and a batch proof showing they were signed by the public key type GetClaimResponse struct { SignedCreds jsonutils.JSONStringArray `json:"signedCreds"` @@ -485,143 +437,19 @@ func MakeSuggestion(service *Service) handlers.AppHandler { }) } -// DrainSuggestionV2Request includes the ID of the verified wallet attempting to drain suggestions -// and returns the drain_poll uuid so the client can poll for status updates on this draining -type DrainSuggestionV2Request struct { - WalletID uuid.UUID `json:"paymentId" valid:"-"` - Credentials []CredentialBinding `json:"credentials"` -} - -// DrainSuggestionV2Response - the response structure of the token draining endpoint v2 -type DrainSuggestionV2Response struct { - DrainID *uuid.UUID `json:"drainId"` -} +var errGone = errors.New("endpoint is gone") // DrainSuggestionV2 is the handler for draining ad suggestions for a verified wallet func DrainSuggestionV2(service *Service) handlers.AppHandler { return handlers.AppHandler(func(w http.ResponseWriter, r *http.Request) *handlers.AppError { - var ( - req DrainSuggestionV2Request - resp = DrainSuggestionV2Response{} - ) - - ctx := r.Context() - // no logger, setup - // get logger from context - logger := logging.Logger(ctx, "wallet.DrainSuggestionV2") - - err := requestutils.ReadJSON(r.Context(), r.Body, &req) - if err != nil { - return handlers.WrapError(err, "Error in request body", http.StatusBadRequest) - } - - sublogger := logger.With(). - Str("wallet_id", req.WalletID.String()). - Logger() - - _, err = govalidator.ValidateStruct(req) - if err != nil { - sublogger.Error().Err(err).Msg("failed to validate request") - return handlers.WrapValidationError(err) - } - - logging.AddWalletIDToContext(r.Context(), req.WalletID) - - keyID, err := middleware.GetKeyID(r.Context()) - sublogger = sublogger.With().Str("key_id", keyID).Logger() - if err != nil { - sublogger.Error().Err(err).Msg("failed to get http signature key id") - return handlers.WrapError(err, "Error looking up http signature info", http.StatusBadRequest) - } - if req.WalletID.String() != keyID { - sublogger.Error().Err(err).Msg("httpsignature key id != wallet id") - return handlers.ValidationError("request", - map[string]string{"paymentId": "paymentId must match signature"}) - } - - drainID, err := service.Drain(ctx, req.Credentials, req.WalletID) - if err != nil { - switch err.(type) { - case govalidator.Error: - sublogger.Error().Err(err).Msg("validation error") - return handlers.WrapValidationError(err) - case govalidator.Errors: - sublogger.Error().Err(err).Msg("validation error") - return handlers.WrapValidationError(err) - default: - // FIXME not all remaining errors should be mapped to 400 - sublogger.Error().Err(err).Msg("error draining") - return handlers.WrapError(err, "Error draining", http.StatusBadRequest) - } - } - resp.DrainID = drainID - - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(resp); err != nil { - panic(err) - } - return nil + return handlers.WrapError(errGone, "gone", http.StatusGone) }) } -// DrainSuggestionRequest includes the ID of the verified wallet attempting to drain suggestions -type DrainSuggestionRequest struct { - WalletID uuid.UUID `json:"paymentId" valid:"-"` - Credentials []CredentialBinding `json:"credentials"` -} - // DrainSuggestion is the handler for draining ad suggestions for a verified wallet func DrainSuggestion(service *Service) handlers.AppHandler { return handlers.AppHandler(func(w http.ResponseWriter, r *http.Request) *handlers.AppError { - var req DrainSuggestionRequest - - ctx := r.Context() - // no logger, setup - // get logger from context - logger := logging.Logger(ctx, "wallet.DrainSuggestion") - - err := requestutils.ReadJSON(r.Context(), r.Body, &req) - if err != nil { - return handlers.WrapError(err, "Error in request body", http.StatusBadRequest) - } - - sublogger := logger.With().Str("wallet_id", req.WalletID.String()).Logger() - - _, err = govalidator.ValidateStruct(req) - if err != nil { - sublogger.Error().Err(err).Msg("validating request body") - return handlers.WrapValidationError(err) - } - - logging.AddWalletIDToContext(r.Context(), req.WalletID) - - keyID, err := middleware.GetKeyID(r.Context()) - if err != nil { - sublogger.Error().Err(err).Msg("error getting keyid from http signature") - return handlers.WrapError(err, "Error looking up http signature info", http.StatusBadRequest) - } - if req.WalletID.String() != keyID { - sublogger.Error().Err(err).Msg("keyid doesnt match wallet in url") - return handlers.ValidationError("request", - map[string]string{"paymentId": "paymentId must match signature"}) - } - - _, err = service.Drain(r.Context(), req.Credentials, req.WalletID) - if err != nil { - sublogger.Error().Err(err).Msg("failed to drain") - switch err.(type) { - case govalidator.Error: - return handlers.WrapValidationError(err) - case govalidator.Errors: - return handlers.WrapValidationError(err) - default: - // FIXME not all remaining errors should be mapped to 400 - return handlers.WrapError(err, "Error draining", http.StatusBadRequest) - } - } - - w.WriteHeader(http.StatusOK) - return nil + return handlers.WrapError(errGone, "gone", http.StatusGone) }) } @@ -845,36 +673,7 @@ type CustodianDrainInfoResponse struct { // GetCustodianDrainInfo is the handler which provides information about a particular paymentId's drains func GetCustodianDrainInfo(service *Service) handlers.AppHandler { return handlers.AppHandler(func(w http.ResponseWriter, r *http.Request) *handlers.AppError { - - var paymentID = new(inputs.ID) - if err := inputs.DecodeAndValidateString(context.Background(), paymentID, chi.URLParam(r, "paymentId")); err != nil { - return handlers.ValidationError( - "Error validating request url parameter", - map[string]interface{}{ - "paymentId": err.Error(), - }, - ) - } - - var resp = &CustodianDrainInfoResponse{} - - drainInfo, err := service.Datastore.GetCustodianDrainInfo(paymentID.UUID()) - if err != nil { - return handlers.WrapError(err, "Error getting custodian drain info payment id", http.StatusBadRequest) - } - - if drainInfo == nil { - return &handlers.AppError{ - Message: "Drain Info does not exist", - Code: http.StatusNotFound, - Data: map[string]interface{}{}, - } - } - - resp.Drains = drainInfo - resp.Meta.Status = "success" - - return handlers.RenderContent(r.Context(), resp, w, http.StatusOK) + return handlers.WrapError(errGone, "gone", http.StatusGone) }) } @@ -886,41 +685,8 @@ type DrainJobRequest struct { // PatchDrainJobErred is the handler for toggling a drain job as retriable func PatchDrainJobErred(service *Service) handlers.AppHandler { return func(w http.ResponseWriter, r *http.Request) *handlers.AppError { - - walletID, err := uuid.FromString(chi.URLParam(r, "walletId")) - if err != nil { - return handlers.ValidationError("validation error", map[string]string{ - "walletId": "must be a valid uuid v4", - }) - } - - var drainJobRequest DrainJobRequest - err = requestutils.ReadJSON(r.Context(), r.Body, &drainJobRequest) - if err != nil { - return handlers.WrapError(errors.New("could not decode request body"), "patch drain job", - http.StatusBadRequest) - } - - if drainJobRequest.Erred { - return handlers.ValidationError("validation error", map[string]string{ - "erred": "invalid value true only false is supported", - }) - } - - err = service.Datastore.UpdateDrainJobAsRetriable(r.Context(), walletID) - if err != nil { - logging.FromContext(r.Context()).Err(err).Msg("patch drain job") - switch { - case errors.Is(err, errorutils.ErrNotFound): - return handlers.WrapError(fmt.Errorf("no updateable drain job found for walletId %s", walletID), - "patch drain job", http.StatusNotFound) - default: - return handlers.WrapError(fmt.Errorf("error updating drain job for walletdId %s", walletID), - "patch drain job", http.StatusInternalServerError) - } - } - - w.WriteHeader(http.StatusNoContent) + w.WriteHeader(http.StatusGone) + w.Write([]byte{}) return nil } } diff --git a/services/promotion/controllers_test.go b/services/promotion/controllers_test.go index e5e82d3c1..e1be88717 100644 --- a/services/promotion/controllers_test.go +++ b/services/promotion/controllers_test.go @@ -12,30 +12,21 @@ import ( "fmt" "net/http" "net/http/httptest" - "net/http/httputil" "os" "strconv" "strings" "testing" "time" - "github.com/brave-intl/bat-go/libs/handlers" - - mockbitflyer "github.com/brave-intl/bat-go/libs/clients/bitflyer/mock" - errorutils "github.com/brave-intl/bat-go/libs/errors" - "github.com/brave-intl/bat-go/libs/altcurrency" - "github.com/brave-intl/bat-go/libs/clients/bitflyer" "github.com/brave-intl/bat-go/libs/clients/cbr" mockcb "github.com/brave-intl/bat-go/libs/clients/cbr/mock" mockreputation "github.com/brave-intl/bat-go/libs/clients/reputation/mock" - appctx "github.com/brave-intl/bat-go/libs/context" "github.com/brave-intl/bat-go/libs/httpsignature" "github.com/brave-intl/bat-go/libs/jsonutils" kafkautils "github.com/brave-intl/bat-go/libs/kafka" "github.com/brave-intl/bat-go/libs/middleware" walletutils "github.com/brave-intl/bat-go/libs/wallet" - "github.com/brave-intl/bat-go/libs/wallet/provider/uphold" "github.com/brave-intl/bat-go/services/wallet" "github.com/go-chi/chi" "github.com/golang/mock/gomock" @@ -524,468 +515,143 @@ func (suite *ControllersTestSuite) TestClaimGrant() { suite.Require().Equal(http.StatusOK, rr.Code) } -func (suite *ControllersTestSuite) TestSuggestCBRError() { +func (suite *ControllersTestSuite) TestGetClaimSummary() { pg, _, err := NewPostgres() suite.Require().NoError(err, "Failed to get postgres conn") - walletDB, _, err := wallet.NewPostgres() suite.Require().NoError(err, "Failed to get postgres conn") - // Set a random suggestion topic each so the test suite doesn't fail when re-ran - SetSuggestionTopic(uuid.NewV4().String() + ".grant.suggestion") - - // FIXME stick kafka setup in suite setup - kafkaBrokers := os.Getenv("KAFKA_BROKERS") - - dialer, _, err := kafkautils.TLSDialer() - suite.Require().NoError(err) - conn, err := dialer.DialLeader(context.Background(), "tcp", strings.Split(kafkaBrokers, ",")[0], "suggestion", 0) - suite.Require().NoError(err) + service := &Service{ + Datastore: pg, + wallet: &wallet.Service{ + Datastore: walletDB, + }, + } - err = conn.CreateTopics(kafka.TopicConfig{Topic: suggestionTopic, NumPartitions: 1, ReplicationFactor: 1}) - suite.Require().NoError(err) + missingWalletID := uuid.NewV4().String() + body, code := suite.checkGetClaimSummary(service, missingWalletID, "ads") + suite.Require().Equal(http.StatusNotFound, code, "a 404 is sent back") + suite.Assert().JSONEq(`{ + "code": 404, + "message": "Error finding wallet: wallet not found id: '`+missingWalletID+`'" + }`, body, "an error is returned") - mockCtrl := gomock.NewController(suite.T()) - defer mockCtrl.Finish() + publicKey := "hBrtClwIppLmu/qZ8EhGM1TQZUwDUosbOrVu3jMwryY=" + blindedCreds := jsonutils.JSONStringArray([]string{publicKey}) + walletID := uuid.NewV4().String() + info := &walletutils.Info{ + ID: walletID, + Provider: "uphold", + ProviderID: uuid.NewV4().String(), + PublicKey: publicKey, + } + err = service.wallet.Datastore.UpsertWallet(context.Background(), info) + suite.Require().NoError(err, "the wallet failed to be inserted") - publicKey, privKey, err := httpsignature.GenerateEd25519Key(nil) - suite.Require().NoError(err, "Failed to create wallet keypair") + // no content returns an empty string on protocol level + body, code = suite.checkGetClaimSummary(service, walletID, "ads") + suite.Assert().Equal(``, body) + suite.Require().Equal(http.StatusNoContent, code) - walletID := uuid.NewV4() - bat := altcurrency.BAT - info := walletutils.Info{ - ID: walletID.String(), - Provider: "uphold", - ProviderID: "-", - AltCurrency: &bat, - PublicKey: hex.EncodeToString(publicKey), - LastBalance: nil, - } + body, code = suite.checkGetClaimSummary(service, "", "ads") + suite.Assert().JSONEq(`{ + "message": "Error validating query parameter", + "code": 400, + "data": { + "validationErrors": { + "paymentId": "must be a uuidv4" + } + } + }`, body, "body should return a payment id validation error") + suite.Require().Equal(http.StatusBadRequest, code) - mockReputation := mockreputation.NewMockClient(mockCtrl) - mockReputation.EXPECT().IsWalletReputable( - gomock.Any(), - gomock.Any(), - gomock.Any(), - ).Return( - true, - nil, - ) - err = walletDB.UpsertWallet(context.Background(), &info) - suite.Require().NoError(err, "Failed to insert wallet") + // not ignored promotion + promotion, issuer, claim := suite.setupAdsClaim(service, info, 0) - mockCB := mockcb.NewMockClient(mockCtrl) + _, err = pg.ClaimForWallet(promotion, issuer, info, blindedCreds) + suite.Require().NoError(err, "apply claim to wallet") - service := &Service{ - Datastore: pg, - cbClient: mockCB, - wallet: &wallet.Service{ - Datastore: walletDB, - }, - reputationClient: mockReputation, - } + body, code = suite.checkGetClaimSummary(service, walletID, "ads") + suite.Require().Equal(http.StatusOK, code) + suite.Assert().JSONEq(`{ + "amount": "30", + "earnings": "30", + "lastClaim": "`+claim.CreatedAt.Format(time.RFC3339Nano)+`", + "type": "ads" + }`, body, "expected a aggregated claim response") - err = service.InitKafka(context.Background()) - suite.Require().NoError(err, "Failed to initialize kafka") + // ignored promotion (brave transfer + priorClaim := claim + promotion, issuer, claim = suite.setupAdsClaim(service, info, 0) + // set this promotion as a transfer promotion id, and some other random uuid to have more than one + os.Setenv("BRAVE_TRANSFER_PROMOTION_IDS", + fmt.Sprintf("%s %s", promotion.ID.String(), "d41ba588-ab18-4300-a180-d2dc01a22371")) - promotion, err := service.Datastore.CreatePromotion("ugp", 2, decimal.NewFromFloat(0.25), "") - suite.Require().NoError(err, "Failed to create promotion") - err = service.Datastore.ActivatePromotion(promotion) - suite.Require().NoError(err, "Failed to activate promotion") + _, err = pg.ClaimForWallet(promotion, issuer, info, blindedCreds) + suite.Require().NoError(err, "apply claim to wallet") - issuerName := promotion.ID.String() + ":control" - issuerPublicKey := "dHuiBIasUO0khhXsWgygqpVasZhtQraDSZxzJW2FKQ4=" - blindedCreds := []string{"XhBPMjh4vMw+yoNjE7C5OtoTz2rCtfuOXO/Vk7UwWzY="} - signedCreds := []string{"NJnOyyL6YAKMYo6kSAuvtG+/04zK1VNaD9KdKwuzAjU="} - proof := "IiKqfk10e7SJ54Ud/8FnCf+sLYQzS4WiVtYAM5+RVgApY6B9x4CVbMEngkDifEBRD6szEqnNlc3KA8wokGV5Cw==" - sig := "PsavkSWaqsTzZjmoDBmSu6YxQ7NZVrs2G8DQ+LkW5xOejRF6whTiuUJhr9dJ1KlA+79MDbFeex38X5KlnLzvJw==" - preimage := "125KIuuwtHGEl35cb5q1OLSVepoDTgxfsvwTc7chSYUM2Zr80COP19EuMpRQFju1YISHlnB04XJzZYN2ieT9Ng==" + body, code = suite.checkGetClaimSummary(service, walletID, "ads") + suite.Require().Equal(http.StatusOK, code) + // assert you get existing values + suite.Assert().JSONEq(`{ + "amount": "30", + "earnings": "30", + "lastClaim": "`+priorClaim.CreatedAt.Format(time.RFC3339Nano)+`", + "type": "ads" + }`, body, "expected a aggregated claim response") - mockCB.EXPECT().CreateIssuer(gomock.Any(), gomock.Eq(issuerName), gomock.Eq(defaultMaxTokensPerIssuer)).Return(nil) - mockCB.EXPECT().GetIssuer(gomock.Any(), gomock.Eq(issuerName)).Return(&cbr.IssuerResponse{ - Name: issuerName, - PublicKey: issuerPublicKey, - }, nil) - mockCB.EXPECT().SignCredentials(gomock.Any(), gomock.Eq(issuerName), gomock.Eq(blindedCreds)).Return(&cbr.CredentialsIssueResponse{ - BatchProof: proof, - SignedTokens: signedCreds, - }, nil) + // not ignored bonus promotion + promotion, issuer, claim = suite.setupAdsClaim(service, info, 20) - err = walletDB.UpsertWallet(context.Background(), &info) - suite.Require().NoError(err, "Failed to insert wallet") + _, err = pg.ClaimForWallet(promotion, issuer, info, blindedCreds) + suite.Require().NoError(err, "apply claim to wallet") - claimID := suite.ClaimPromotion(service, info, privKey, promotion, blindedCreds, http.StatusOK) - suite.WaitForClaimToPropagate(service, promotion, claimID) + body, code = suite.checkGetClaimSummary(service, walletID, "ads") + suite.Require().Equal(http.StatusOK, code) + suite.Assert().JSONEq(`{ + "amount": "40", + "earnings": "40", + "lastClaim": "`+claim.CreatedAt.Format(time.RFC3339Nano)+`", + "type": "ads" + }`, body, "expected a aggregated claim response") +} - handler := MakeSuggestion(service) +func (suite *ControllersTestSuite) setupAdsClaim(service *Service, w *walletutils.Info, claimBonus float64) (*Promotion, *Issuer, *Claim) { + // promo amount can be different than individual grant amount + promoAmount := decimal.NewFromFloat(25.0) + promotion, err := service.Datastore.CreatePromotion("ads", 2, promoAmount, "") + suite.Require().NoError(err, "a promotion could not be created") - suggestion := Suggestion{ - Type: "oneoff-tip", - Channel: "brave.com", - } + publicKey := "dHuiBIasUO0khhXsWgygqpVasZhtQraDSZxzJW2FKQ4=" + issuer := &Issuer{PromotionID: promotion.ID, Cohort: "control", PublicKey: publicKey} + issuer, err = service.Datastore.InsertIssuer(issuer) + suite.Require().NoError(err, "Insert issuer should succeed") - suggestionBytes, err := json.Marshal(&suggestion) - suite.Require().NoError(err) - suggestionPayload := base64.StdEncoding.EncodeToString(suggestionBytes) + err = service.Datastore.ActivatePromotion(promotion) + suite.Require().NoError(err, "a promotion should be activated") - suggestionReq := SuggestionRequest{ - Suggestion: suggestionPayload, - Credentials: []CredentialBinding{{ - PublicKey: issuerPublicKey, - Signature: sig, - TokenPreimage: preimage, - }}, - } + grantAmount := decimal.NewFromFloat(30.0) + claim, err := service.Datastore.CreateClaim(promotion.ID, w.ID, grantAmount, decimal.NewFromFloat(claimBonus), false) + suite.Require().NoError(err, "create a claim for a promotion") - // return a duplicate redemption error from CBR to test out our codified messages - mockCB.EXPECT().RedeemCredentials(gomock.Any(), gomock.Eq([]cbr.CredentialRedemption{{ - Issuer: issuerName, - TokenPreimage: preimage, - Signature: sig, - }}), gomock.Eq(suggestionPayload)).Return( - errorutils.New(err, "cbr duplicate redemption", - errorutils.Codified{ - ErrCode: "cbr_dup_redeem", - Retry: false, - })) + return promotion, issuer, claim +} - body, err := json.Marshal(&suggestionReq) +func (suite *ControllersTestSuite) checkGetClaimSummary(service *Service, walletID string, claimType string) (string, int) { + handler := GetClaimSummary(service) + req, err := http.NewRequest("GET", "/promotion/{claimType}/grants/total?paymentId="+walletID, nil) suite.Require().NoError(err) - req, err := http.NewRequest("POST", "/suggestion", bytes.NewBuffer(body)) - suite.Require().NoError(err) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("claimType", claimType) + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) - suite.Require().Equal(http.StatusOK, rr.Code) - - // wait for the job to be processed - <-time.After(2 * time.Second) - // check that the suggestion drain got the right error - var failedSuggestion = getSuggestionDrainEntry(pg.(*DatastoreWithPrometheus).base.(*Postgres)) - suite.Require().Equal("cbr_dup_redeem", *failedSuggestion.ErrCode) - suite.Require().Equal(true, failedSuggestion.Erred) - + return rr.Body.String(), rr.Code } -func (suite *ControllersTestSuite) TestSuggest() { - pg, _, err := NewPostgres() - suite.Require().NoError(err, "Failed to get postgres conn") - - walletDB, _, err := wallet.NewPostgres() - suite.Require().NoError(err, "Failed to get postgres conn") - - // Set a random suggestion topic each so the test suite doesn't fail when re-ran - SetSuggestionTopic(uuid.NewV4().String() + ".grant.suggestion") - - // FIXME stick kafka setup in suite setup - kafkaBrokers := os.Getenv("KAFKA_BROKERS") - - dialer, _, err := kafkautils.TLSDialer() - suite.Require().NoError(err) - conn, err := dialer.DialLeader(context.Background(), "tcp", strings.Split(kafkaBrokers, ",")[0], "suggestion", 0) - suite.Require().NoError(err) - - err = conn.CreateTopics(kafka.TopicConfig{Topic: suggestionTopic, NumPartitions: 1, ReplicationFactor: 1}) - suite.Require().NoError(err) - - offset, err := conn.ReadLastOffset() - suite.Require().NoError(err) - - mockCtrl := gomock.NewController(suite.T()) - defer mockCtrl.Finish() - - publicKey, privKey, err := httpsignature.GenerateEd25519Key(nil) - suite.Require().NoError(err, "Failed to create wallet keypair") - - walletID := uuid.NewV4() - bat := altcurrency.BAT - info := walletutils.Info{ - ID: walletID.String(), - Provider: "uphold", - ProviderID: "-", - AltCurrency: &bat, - PublicKey: hex.EncodeToString(publicKey), - LastBalance: nil, - } - - mockReputation := mockreputation.NewMockClient(mockCtrl) - mockReputation.EXPECT().IsWalletReputable( - gomock.Any(), - gomock.Any(), - gomock.Any(), - ).Return( - true, - nil, - ) - err = walletDB.UpsertWallet(context.Background(), &info) - suite.Require().NoError(err, "Failed to insert wallet") - - mockCB := mockcb.NewMockClient(mockCtrl) - - service := &Service{ - Datastore: pg, - cbClient: mockCB, - wallet: &wallet.Service{ - Datastore: walletDB, - }, - reputationClient: mockReputation, - } - - err = service.InitKafka(context.Background()) - suite.Require().NoError(err, "Failed to initialize kafka") - - promotion, err := service.Datastore.CreatePromotion("ugp", 2, decimal.NewFromFloat(0.25), "") - suite.Require().NoError(err, "Failed to create promotion") - err = service.Datastore.ActivatePromotion(promotion) - suite.Require().NoError(err, "Failed to activate promotion") - - issuerName := promotion.ID.String() + ":control" - issuerPublicKey := "dHuiBIasUO0khhXsWgygqpVasZhtQraDSZxzJW2FKQ4=" - blindedCreds := []string{"XhBPMjh4vMw+yoNjE7C5OtoTz2rCtfuOXO/Vk7UwWzY="} - signedCreds := []string{"NJnOyyL6YAKMYo6kSAuvtG+/04zK1VNaD9KdKwuzAjU="} - proof := "IiKqfk10e7SJ54Ud/8FnCf+sLYQzS4WiVtYAM5+RVgApY6B9x4CVbMEngkDifEBRD6szEqnNlc3KA8wokGV5Cw==" - sig := "PsavkSWaqsTzZjmoDBmSu6YxQ7NZVrs2G8DQ+LkW5xOejRF6whTiuUJhr9dJ1KlA+79MDbFeex38X5KlnLzvJw==" - preimage := "125KIuuwtHGEl35cb5q1OLSVepoDTgxfsvwTc7chSYUM2Zr80COP19EuMpRQFju1YISHlnB04XJzZYN2ieT9Ng==" - - mockCB.EXPECT().CreateIssuer(gomock.Any(), gomock.Eq(issuerName), gomock.Eq(defaultMaxTokensPerIssuer)).Return(nil) - mockCB.EXPECT().GetIssuer(gomock.Any(), gomock.Eq(issuerName)).Return(&cbr.IssuerResponse{ - Name: issuerName, - PublicKey: issuerPublicKey, - }, nil) - mockCB.EXPECT().SignCredentials(gomock.Any(), gomock.Eq(issuerName), gomock.Eq(blindedCreds)).Return(&cbr.CredentialsIssueResponse{ - BatchProof: proof, - SignedTokens: signedCreds, - }, nil) - - err = walletDB.UpsertWallet(context.Background(), &info) - suite.Require().NoError(err, "Failed to insert wallet") - - claimID := suite.ClaimPromotion(service, info, privKey, promotion, blindedCreds, http.StatusOK) - suite.WaitForClaimToPropagate(service, promotion, claimID) - - handler := MakeSuggestion(service) - - suggestion := Suggestion{ - Type: "oneoff-tip", - Channel: "brave.com", - } - - suggestionBytes, err := json.Marshal(&suggestion) - suite.Require().NoError(err) - suggestionPayload := base64.StdEncoding.EncodeToString(suggestionBytes) - - suggestionReq := SuggestionRequest{ - Suggestion: suggestionPayload, - Credentials: []CredentialBinding{{ - PublicKey: issuerPublicKey, - Signature: sig, - TokenPreimage: preimage, - }}, - } - - mockCB.EXPECT().RedeemCredentials(gomock.Any(), gomock.Eq([]cbr.CredentialRedemption{{ - Issuer: issuerName, - TokenPreimage: preimage, - Signature: sig, - }}), gomock.Eq(suggestionPayload)).Return(nil) - - body, err := json.Marshal(&suggestionReq) - suite.Require().NoError(err) - - req, err := http.NewRequest("POST", "/suggestion", bytes.NewBuffer(body)) - suite.Require().NoError(err) - - r := kafka.NewReader(kafka.ReaderConfig{ - Brokers: strings.Split(kafkaBrokers, ","), - Topic: suggestionTopic, - Dialer: service.kafkaDialer, - MaxWait: time.Second, - RebalanceTimeout: time.Second, - Logger: kafka.LoggerFunc(log.Printf), - }) - codec := service.codecs["suggestion"] - - // :cry: - - err = r.SetOffset(offset) - suite.Require().NoError(err) - - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - suite.Require().Equal(http.StatusOK, rr.Code) - - suggestionEventBinary, err := r.ReadMessage(context.Background()) - suite.Require().NoError(err) - - suggestionEvent, _, err := codec.NativeFromBinary(suggestionEventBinary.Value) - suite.Require().NoError(err) - - suggestionEventJSON, err := codec.TextualFromNative(nil, suggestionEvent) - suite.Require().NoError(err) - - eventMap, ok := suggestionEvent.(map[string]interface{}) - suite.Require().True(ok) - id, ok := eventMap["id"].(string) - suite.Require().True(ok) - createdAt, ok := eventMap["createdAt"].(string) - suite.Require().True(ok) - - suite.Assert().JSONEq(`{ - "id": "`+id+`", - "createdAt": "`+createdAt+`", - "type": "`+suggestion.Type+`", - "channel": "`+suggestion.Channel+`", - "totalAmount": "0.25", - "orderId": "", - "funding": [ - { - "type": "ugp", - "amount": "0.25", - "cohort": "control", - "promotion": "`+promotion.ID.String()+`" - } - ] - }`, string(suggestionEventJSON), "Incorrect suggestion event") -} - -func (suite *ControllersTestSuite) TestGetClaimSummary() { - pg, _, err := NewPostgres() - suite.Require().NoError(err, "Failed to get postgres conn") - walletDB, _, err := wallet.NewPostgres() - suite.Require().NoError(err, "Failed to get postgres conn") - - service := &Service{ - Datastore: pg, - wallet: &wallet.Service{ - Datastore: walletDB, - }, - } - - missingWalletID := uuid.NewV4().String() - body, code := suite.checkGetClaimSummary(service, missingWalletID, "ads") - suite.Require().Equal(http.StatusNotFound, code, "a 404 is sent back") - suite.Assert().JSONEq(`{ - "code": 404, - "message": "Error finding wallet: wallet not found id: '`+missingWalletID+`'" - }`, body, "an error is returned") - - publicKey := "hBrtClwIppLmu/qZ8EhGM1TQZUwDUosbOrVu3jMwryY=" - blindedCreds := jsonutils.JSONStringArray([]string{publicKey}) - walletID := uuid.NewV4().String() - info := &walletutils.Info{ - ID: walletID, - Provider: "uphold", - ProviderID: uuid.NewV4().String(), - PublicKey: publicKey, - } - err = service.wallet.Datastore.UpsertWallet(context.Background(), info) - suite.Require().NoError(err, "the wallet failed to be inserted") - - // no content returns an empty string on protocol level - body, code = suite.checkGetClaimSummary(service, walletID, "ads") - suite.Assert().Equal(``, body) - suite.Require().Equal(http.StatusNoContent, code) - - body, code = suite.checkGetClaimSummary(service, "", "ads") - suite.Assert().JSONEq(`{ - "message": "Error validating query parameter", - "code": 400, - "data": { - "validationErrors": { - "paymentId": "must be a uuidv4" - } - } - }`, body, "body should return a payment id validation error") - suite.Require().Equal(http.StatusBadRequest, code) - - // not ignored promotion - promotion, issuer, claim := suite.setupAdsClaim(service, info, 0) - - _, err = pg.ClaimForWallet(promotion, issuer, info, blindedCreds) - suite.Require().NoError(err, "apply claim to wallet") - - body, code = suite.checkGetClaimSummary(service, walletID, "ads") - suite.Require().Equal(http.StatusOK, code) - suite.Assert().JSONEq(`{ - "amount": "30", - "earnings": "30", - "lastClaim": "`+claim.CreatedAt.Format(time.RFC3339Nano)+`", - "type": "ads" - }`, body, "expected a aggregated claim response") - - // ignored promotion (brave transfer - priorClaim := claim - promotion, issuer, claim = suite.setupAdsClaim(service, info, 0) - // set this promotion as a transfer promotion id, and some other random uuid to have more than one - os.Setenv("BRAVE_TRANSFER_PROMOTION_IDS", - fmt.Sprintf("%s %s", promotion.ID.String(), "d41ba588-ab18-4300-a180-d2dc01a22371")) - - _, err = pg.ClaimForWallet(promotion, issuer, info, blindedCreds) - suite.Require().NoError(err, "apply claim to wallet") - - body, code = suite.checkGetClaimSummary(service, walletID, "ads") - suite.Require().Equal(http.StatusOK, code) - // assert you get existing values - suite.Assert().JSONEq(`{ - "amount": "30", - "earnings": "30", - "lastClaim": "`+priorClaim.CreatedAt.Format(time.RFC3339Nano)+`", - "type": "ads" - }`, body, "expected a aggregated claim response") - - // not ignored bonus promotion - promotion, issuer, claim = suite.setupAdsClaim(service, info, 20) - - _, err = pg.ClaimForWallet(promotion, issuer, info, blindedCreds) - suite.Require().NoError(err, "apply claim to wallet") - - body, code = suite.checkGetClaimSummary(service, walletID, "ads") - suite.Require().Equal(http.StatusOK, code) - suite.Assert().JSONEq(`{ - "amount": "40", - "earnings": "40", - "lastClaim": "`+claim.CreatedAt.Format(time.RFC3339Nano)+`", - "type": "ads" - }`, body, "expected a aggregated claim response") -} - -func (suite *ControllersTestSuite) setupAdsClaim(service *Service, w *walletutils.Info, claimBonus float64) (*Promotion, *Issuer, *Claim) { - // promo amount can be different than individual grant amount - promoAmount := decimal.NewFromFloat(25.0) - promotion, err := service.Datastore.CreatePromotion("ads", 2, promoAmount, "") - suite.Require().NoError(err, "a promotion could not be created") - - publicKey := "dHuiBIasUO0khhXsWgygqpVasZhtQraDSZxzJW2FKQ4=" - issuer := &Issuer{PromotionID: promotion.ID, Cohort: "control", PublicKey: publicKey} - issuer, err = service.Datastore.InsertIssuer(issuer) - suite.Require().NoError(err, "Insert issuer should succeed") - - err = service.Datastore.ActivatePromotion(promotion) - suite.Require().NoError(err, "a promotion should be activated") - - grantAmount := decimal.NewFromFloat(30.0) - claim, err := service.Datastore.CreateClaim(promotion.ID, w.ID, grantAmount, decimal.NewFromFloat(claimBonus), false) - suite.Require().NoError(err, "create a claim for a promotion") - - return promotion, issuer, claim -} - -func (suite *ControllersTestSuite) checkGetClaimSummary(service *Service, walletID string, claimType string) (string, int) { - handler := GetClaimSummary(service) - req, err := http.NewRequest("GET", "/promotion/{claimType}/grants/total?paymentId="+walletID, nil) - suite.Require().NoError(err) - - rctx := chi.NewRouteContext() - rctx.URLParams.Add("claimType", claimType) - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) - - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - return rr.Body.String(), rr.Code -} - -func (suite *ControllersTestSuite) TestCreatePromotion() { +func (suite *ControllersTestSuite) TestCreatePromotion() { pg, _, err := NewPostgres() suite.Require().NoError(err, "Failed to get postgres conn") @@ -1376,1607 +1042,14 @@ func (suite *ControllersTestSuite) TestClaimCompatibility() { } } -func (suite *ControllersTestSuite) TestSuggestionMintDrain() { +// THIS CODE IS A QUICK AND DIRTY HACK +// WE SHOULD DELETE ALL OF THIS AND MOVE OVER TO THE PAYMENT SERVICE ONCE DEMO IS DONE. + +// CreateOrder creates orders given the total price, merchant ID, status and items of the order +func (suite *ControllersTestSuite) CreateOrder() (string, error) { pg, _, err := NewPostgres() - suite.Require().NoError(err, "Failed to get postgres conn") - - walletDB, _, err := wallet.NewPostgres() - suite.Require().NoError(err, "Failed to get postgres conn") - - ch := make(chan *walletutils.TransactionInfo) - - mockCtrl := gomock.NewController(suite.T()) - defer mockCtrl.Finish() - - publicKey, privKey, err := httpsignature.GenerateEd25519Key(nil) - suite.Require().NoError(err, "Failed to create wallet keypair") - - walletID := uuid.NewV4() - walletID2 := uuid.NewV4() - - bat := altcurrency.BAT - info := walletutils.Info{ - ID: walletID.String(), - Provider: "uphold", - ProviderID: "-", - AltCurrency: &bat, - PublicKey: hex.EncodeToString(publicKey), - LastBalance: nil, - UserDepositDestination: walletID2.String(), - } - info.UserDepositAccountProvider = new(string) - *info.UserDepositAccountProvider = "brave" - - info2 := walletutils.Info{ - ID: walletID2.String(), - Provider: "uphold", - ProviderID: "-", - AltCurrency: &bat, - PublicKey: hex.EncodeToString(publicKey), - LastBalance: nil, - } - - mockReputation := mockreputation.NewMockClient(mockCtrl) - mockReputation.EXPECT().IsWalletReputable( - gomock.Any(), - gomock.Any(), - gomock.Any(), - ).Return( - true, - nil, - ) - // drain reputation check - mockReputation.EXPECT().IsWalletAdsReputable( - gomock.Any(), - gomock.Any(), - gomock.Any(), - ).Return( - true, - nil, - ) - mockReputation.EXPECT().IsWalletOnPlatform( - gomock.Any(), - gomock.Any(), - gomock.Any(), - ).Return( - true, - nil, - ) - mockCB := mockcb.NewMockClient(mockCtrl) - - service := &Service{ - Datastore: pg, - cbClient: mockCB, - wallet: &wallet.Service{ - Datastore: walletDB, - }, - reputationClient: mockReputation, - drainChannel: ch, - } - - tidpromotion, err := service.Datastore.CreatePromotion("ads", 2, decimal.NewFromFloat(0.25), "") - suite.Require().NoError(err, "Failed to create promotion") - err = service.Datastore.ActivatePromotion(tidpromotion) - suite.Require().NoError(err, "Failed to activate promotion") - - promotion, err := service.Datastore.CreatePromotion("ads", 2, decimal.NewFromFloat(0.25), "") - suite.Require().NoError(err, "Failed to create promotion") - err = service.Datastore.ActivatePromotion(promotion) - suite.Require().NoError(err, "Failed to activate promotion") - - err = service.wallet.Datastore.UpsertWallet(context.Background(), &info) - suite.Require().NoError(err, "the wallet failed to be inserted") - - err = service.wallet.Datastore.UpsertWallet(context.Background(), &info2) - suite.Require().NoError(err, "the wallet failed to be inserted") - - claimBonus := 0.25 - grantAmount := decimal.NewFromFloat(0.25) - _, err = service.Datastore.CreateClaim(promotion.ID, info.ID, grantAmount, decimal.NewFromFloat(claimBonus), false) - suite.Require().NoError(err, "create a claim for a promotion") - - issuerName := promotion.ID.String() + ":control" - issuerPublicKey := "dHuiBIasUO0khhXsWgygqpVasZhtQraDSZxzJW2FKQ4=" - blindedCreds := []string{"XhBPMjh4vMw+yoNjE7C5OtoTz2rCtfuOXO/Vk7UwWzY="} - signedCreds := []string{"NJnOyyL6YAKMYo6kSAuvtG+/04zK1VNaD9KdKwuzAjU="} - proof := "IiKqfk10e7SJ54Ud/8FnCf+sLYQzS4WiVtYAM5+RVgApY6B9x4CVbMEngkDifEBRD6szEqnNlc3KA8wokGV5Cw==" - sig := "PsavkSWaqsTzZjmoDBmSu6YxQ7NZVrs2G8DQ+LkW5xOejRF6whTiuUJhr9dJ1KlA+79MDbFeex38X5KlnLzvJw==" - preimage := "125KIuuwtHGEl35cb5q1OLSVepoDTgxfsvwTc7chSYUM2Zr80COP19EuMpRQFju1YISHlnB04XJzZYN2ieT9Ng==" - - mockCB.EXPECT().CreateIssuer(gomock.Any(), gomock.Eq(issuerName), gomock.Eq(defaultMaxTokensPerIssuer)).Return(nil) - mockCB.EXPECT().GetIssuer(gomock.Any(), gomock.Eq(issuerName)).Return(&cbr.IssuerResponse{ - Name: issuerName, - PublicKey: issuerPublicKey, - }, nil) - mockCB.EXPECT().SignCredentials(gomock.Any(), gomock.Eq(issuerName), gomock.Eq(blindedCreds)).Return(&cbr.CredentialsIssueResponse{ - BatchProof: proof, - SignedTokens: signedCreds, - }, nil) - - claimID := suite.ClaimPromotion(service, info, privKey, promotion, blindedCreds, http.StatusOK) - suite.WaitForClaimToPropagate(service, promotion, claimID) - - mockCB.EXPECT().RedeemCredentials(gomock.Any(), gomock.Eq([]cbr.CredentialRedemption{{ - Issuer: issuerName, - TokenPreimage: preimage, - Signature: sig, - }}), gomock.Eq(walletID.String())).Return(nil) - - handler := middleware.HTTPSignedOnly(service)(DrainSuggestion(service)) - - drainReq := DrainSuggestionRequest{ - WalletID: walletID, - Credentials: []CredentialBinding{{ - PublicKey: issuerPublicKey, - Signature: sig, - TokenPreimage: preimage, - }}, - } - - body, err := json.Marshal(&drainReq) - suite.Require().NoError(err) - - ctx := context.WithValue(context.Background(), appctx.ReputationOnDrainCTXKey, true) - req, err := http.NewRequestWithContext(ctx, "POST", "/suggestion/drain", bytes.NewBuffer(body)) - suite.Require().NoError(err) - - var s httpsignature.SignatureParams - s.Algorithm = httpsignature.ED25519 - s.KeyID = info.ID - s.Headers = []string{"digest", "(request-target)"} - - err = s.Sign(privKey, crypto.Hash(0), req) - suite.Require().NoError(err) - - err = walletDB.InsertWallet(context.Background(), &info) - suite.Require().NoError(err, "Failed to insert wallet") - err = walletDB.InsertWallet(context.Background(), &info2) - suite.Require().NoError(err, "Failed to insert wallet") - - req, err = http.NewRequestWithContext(ctx, "POST", "/suggestion/drain", bytes.NewBuffer(body)) - suite.Require().NoError(err) - - ctx = context.WithValue(req.Context(), appctx.BraveTransferPromotionIDCTXKey, []string{tidpromotion.ID.String()}) - req = req.WithContext(ctx) - - err = s.Sign(privKey, crypto.Hash(0), req) - suite.Require().NoError(err) - - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - b, _ := httputil.DumpResponse(rr.Result(), true) - fmt.Printf("%s", b) - suite.Require().Equal(http.StatusOK, rr.Code) - <-time.After(2 * time.Second) -} - -func (suite *ControllersTestSuite) TestSuggestionDrainBitflyerJPYLimit() { - suite.CleanDB() - // TODO: after we figure out why we are being blocked by bf enable - //suite.T().Skip("bitflyer side unable to settle") - pg, _, err := NewPostgres() - suite.Require().NoError(err, "Failed to get postgres conn") - - walletDB, _, err := wallet.NewPostgres() - suite.Require().NoError(err, "Failed to get postgres conn") - - ch := make(chan *walletutils.TransactionInfo) - - mockCtrl := gomock.NewController(suite.T()) - defer mockCtrl.Finish() - - // setup bf client - var bfClient = mockbitflyer.NewMockClient(mockCtrl) - - priceToken := uuid.NewV4() - JPY := "JPY" - BAT := "BAT" - currencyCode := fmt.Sprintf("%s_%s", BAT, JPY) - - rate, err := decimal.NewFromString("100000000000000.025") - suite.Require().NoError(err) - bfClient.EXPECT(). - FetchQuote(gomock.Any(), currencyCode, false). - Return(&bitflyer.Quote{ - PriceToken: priceToken.String(), - ProductCode: currencyCode, - MainCurrency: JPY, - SubCurrency: BAT, - Rate: rate, - }, nil) - - bfClient.EXPECT(). - UploadBulkPayout( - gomock.Any(), - gomock.Any(), - ). - Return(&bitflyer.WithdrawToDepositIDBulkResponse{ - DryRun: false, - Withdrawals: []bitflyer.WithdrawToDepositIDResponse{{ - CurrencyCode: BAT, - Status: "SUCCESS", - TransferID: "transferid", - }}, - }, nil) - - publicKey, privKey, err := httpsignature.GenerateEd25519Key(nil) - suite.Require().NoError(err, "Failed to create wallet keypair") - - walletID := uuid.NewV4() - bat := altcurrency.BAT - - custodian := "bitflyer" - - info := walletutils.Info{ - ID: walletID.String(), - Provider: "brave", - ProviderID: "-", - AltCurrency: &bat, - PublicKey: hex.EncodeToString(publicKey), - LastBalance: nil, - } - - mockReputation := mockreputation.NewMockClient(mockCtrl) - mockReputation.EXPECT().IsWalletReputable( - gomock.Any(), - gomock.Any(), - gomock.Any(), - ).Return( - true, - nil, - ) - mockReputation.EXPECT().IsWalletAdsReputable( - gomock.Any(), - gomock.Any(), - gomock.Any(), - ).Return( - true, - nil, - ) - mockCB := mockcb.NewMockClient(mockCtrl) - - service := &Service{ - Datastore: pg, - bfClient: bfClient, - cbClient: mockCB, - wallet: &wallet.Service{ - Datastore: walletDB, - }, - reputationClient: mockReputation, - drainChannel: ch, - } - - promotion, err := service.Datastore.CreatePromotion("ads", 2, decimal.NewFromFloat(0.25), "") - suite.Require().NoError(err, "Failed to create promotion") - err = service.Datastore.ActivatePromotion(promotion) - suite.Require().NoError(err, "Failed to activate promotion") - - err = service.wallet.Datastore.UpsertWallet(context.Background(), &info) - suite.Require().NoError(err, "the wallet failed to be inserted") - - claimBonus := 0.25 - grantAmount := decimal.NewFromFloat(0.25) - _, err = service.Datastore.CreateClaim(promotion.ID, info.ID, grantAmount, decimal.NewFromFloat(claimBonus), false) - suite.Require().NoError(err, "create a claim for a promotion") - - issuerName := promotion.ID.String() + ":control" - issuerPublicKey := "dHuiBIasUO0khhXsWgygqpVasZhtQraDSZxzJW2FKQ4=" - blindedCreds := []string{"XhBPMjh4vMw+yoNjE7C5OtoTz2rCtfuOXO/Vk7UwWzY="} - signedCreds := []string{"NJnOyyL6YAKMYo6kSAuvtG+/04zK1VNaD9KdKwuzAjU="} - proof := "IiKqfk10e7SJ54Ud/8FnCf+sLYQzS4WiVtYAM5+RVgApY6B9x4CVbMEngkDifEBRD6szEqnNlc3KA8wokGV5Cw==" - sig := "PsavkSWaqsTzZjmoDBmSu6YxQ7NZVrs2G8DQ+LkW5xOejRF6whTiuUJhr9dJ1KlA+79MDbFeex38X5KlnLzvJw==" - preimage := "125KIuuwtHGEl35cb5q1OLSVepoDTgxfsvwTc7chSYUM2Zr80COP19EuMpRQFju1YISHlnB04XJzZYN2ieT9Ng==" - - mockCB.EXPECT().CreateIssuer(gomock.Any(), gomock.Eq(issuerName), gomock.Eq(defaultMaxTokensPerIssuer)).Return(nil) - mockCB.EXPECT().GetIssuer(gomock.Any(), gomock.Eq(issuerName)).Return(&cbr.IssuerResponse{ - Name: issuerName, - PublicKey: issuerPublicKey, - }, nil) - mockCB.EXPECT().SignCredentials(gomock.Any(), gomock.Eq(issuerName), gomock.Eq(blindedCreds)).Return(&cbr.CredentialsIssueResponse{ - BatchProof: proof, - SignedTokens: signedCreds, - }, nil) - - claimID := suite.ClaimPromotion(service, info, privKey, promotion, blindedCreds, http.StatusOK) - suite.WaitForClaimToPropagate(service, promotion, claimID) - - mockCB.EXPECT().RedeemCredentials(gomock.Any(), gomock.Eq([]cbr.CredentialRedemption{{ - Issuer: issuerName, - TokenPreimage: preimage, - Signature: sig, - }}), gomock.Eq(walletID.String())).Return(nil) - - handler := middleware.HTTPSignedOnly(service)(DrainSuggestion(service)) - - drainReq := DrainSuggestionRequest{ - WalletID: walletID, - Credentials: []CredentialBinding{{ - PublicKey: issuerPublicKey, - Signature: sig, - TokenPreimage: preimage, - }}, - } - - body, err := json.Marshal(&drainReq) - suite.Require().NoError(err) - - ctx := context.WithValue(context.Background(), appctx.ReputationOnDrainCTXKey, true) - req, err := http.NewRequestWithContext(ctx, "POST", "/suggestion/drain", bytes.NewBuffer(body)) - suite.Require().NoError(err) - - var s httpsignature.SignatureParams - s.Algorithm = httpsignature.ED25519 - s.KeyID = info.ID - s.Headers = []string{"digest", "(request-target)"} - - err = s.Sign(privKey, crypto.Hash(0), req) - suite.Require().NoError(err) - - err = walletDB.InsertWallet(context.Background(), &info) - suite.Require().NoError(err, "Failed to insert wallet") - - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - - suite.Require().Equal(http.StatusBadRequest, rr.Code, "Wallet without payout address should fail") - - req, err = http.NewRequestWithContext(ctx, "POST", "/suggestion/drain", bytes.NewBuffer(body)) - suite.Require().NoError(err) - - err = s.Sign(privKey, crypto.Hash(0), req) - suite.Require().NoError(err) - - info.UserDepositDestination = uuid.NewV4().String() - info.UserDepositAccountProvider = &custodian - - err = walletDB.UpsertWallet(context.Background(), &info) - suite.Require().NoError(err, "Failed to insert wallet") - - rr = httptest.NewRecorder() - handler.ServeHTTP(rr, req) - b, _ := httputil.DumpResponse(rr.Result(), true) - fmt.Printf("%s", b) - suite.Require().Equal(http.StatusOK, rr.Code) - - <-ch - <-time.After(1 * time.Second) - - // the runnextbatchpayments job needs to kick in before checking the drain status - attempted, err := service.RunNextBatchPaymentsJob(ctx) - suite.Require().True(attempted) - suite.Require().EqualError(err, "over custodian transfer limit") - - <-time.After(1 * time.Second) - var drainJob = getClaimDrainEntry(pg.(*DatastoreWithPrometheus).base.(*Postgres)) - suite.Require().True(drainJob.Erred) - suite.Require().Equal(*drainJob.ErrCode, "bf_transfer_limit", "error code should be transfer limit") -} - -func (suite *ControllersTestSuite) TestSuggestionDrainSkipCBRDupRedeem() { - pg, _, err := NewPostgres() - suite.Require().NoError(err, "Failed to get postgres conn") - - walletDB, _, err := wallet.NewPostgres() - suite.Require().NoError(err, "Failed to get postgres conn") - - ch := make(chan *walletutils.TransactionInfo) - - mockCtrl := gomock.NewController(suite.T()) - defer mockCtrl.Finish() - - // setup bf client - var bfClient = mockbitflyer.NewMockClient(mockCtrl) - - publicKey, privKey, err := httpsignature.GenerateEd25519Key(nil) - suite.Require().NoError(err, "Failed to create wallet keypair") - - walletID := uuid.NewV4() - bat := altcurrency.BAT - - custodian := "bitflyer" - - info := walletutils.Info{ - ID: walletID.String(), - Provider: "brave", - ProviderID: "-", - AltCurrency: &bat, - PublicKey: hex.EncodeToString(publicKey), - LastBalance: nil, - } - - mockReputation := mockreputation.NewMockClient(mockCtrl) - // the wallet reputation check originally succeeds - mockReputation.EXPECT().IsWalletReputable( - gomock.Any(), - gomock.Any(), - gomock.Any(), - ).Return( - true, - nil, - ) - // the drain reputation check fails - mockReputation.EXPECT().IsWalletAdsReputable( - gomock.Any(), - gomock.Any(), - gomock.Any(), - ).Return( - false, - nil, - ) - mockCB := mockcb.NewMockClient(mockCtrl) - - service := &Service{ - Datastore: pg, - bfClient: bfClient, - cbClient: mockCB, - wallet: &wallet.Service{ - Datastore: walletDB, - }, - reputationClient: mockReputation, - drainChannel: ch, - } - - promotion, err := service.Datastore.CreatePromotion("ads", 2, decimal.NewFromFloat(0.25), "") - suite.Require().NoError(err, "Failed to create promotion") - err = service.Datastore.ActivatePromotion(promotion) - suite.Require().NoError(err, "Failed to activate promotion") - - err = service.wallet.Datastore.UpsertWallet(context.Background(), &info) - suite.Require().NoError(err, "the wallet failed to be inserted") - - claimBonus := 0.25 - grantAmount := decimal.NewFromFloat(0.25) - _, err = service.Datastore.CreateClaim(promotion.ID, info.ID, grantAmount, decimal.NewFromFloat(claimBonus), false) - suite.Require().NoError(err, "create a claim for a promotion") - - issuerName := promotion.ID.String() + ":control" - issuerPublicKey := "dHuiBIasUO0khhXsWgygqpVasZhtQraDSZxzJW2FKQ4=" - blindedCreds := []string{"XhBPMjh4vMw+yoNjE7C5OtoTz2rCtfuOXO/Vk7UwWzY="} - signedCreds := []string{"NJnOyyL6YAKMYo6kSAuvtG+/04zK1VNaD9KdKwuzAjU="} - proof := "IiKqfk10e7SJ54Ud/8FnCf+sLYQzS4WiVtYAM5+RVgApY6B9x4CVbMEngkDifEBRD6szEqnNlc3KA8wokGV5Cw==" - sig := "PsavkSWaqsTzZjmoDBmSu6YxQ7NZVrs2G8DQ+LkW5xOejRF6whTiuUJhr9dJ1KlA+79MDbFeex38X5KlnLzvJw==" - preimage := "125KIuuwtHGEl35cb5q1OLSVepoDTgxfsvwTc7chSYUM2Zr80COP19EuMpRQFju1YISHlnB04XJzZYN2ieT9Ng==" - - mockCB.EXPECT().CreateIssuer(gomock.Any(), gomock.Eq(issuerName), gomock.Eq(defaultMaxTokensPerIssuer)).Return(nil) - mockCB.EXPECT().GetIssuer(gomock.Any(), gomock.Eq(issuerName)).Return(&cbr.IssuerResponse{ - Name: issuerName, - PublicKey: issuerPublicKey, - }, nil) - mockCB.EXPECT().SignCredentials(gomock.Any(), gomock.Eq(issuerName), gomock.Eq(blindedCreds)).Return(&cbr.CredentialsIssueResponse{ - BatchProof: proof, - SignedTokens: signedCreds, - }, nil) - - claimID := suite.ClaimPromotion(service, info, privKey, promotion, blindedCreds, http.StatusOK) - suite.WaitForClaimToPropagate(service, promotion, claimID) - - /* cb should not be called, we are using the bypass redeem credentials feature - mockCB.EXPECT().RedeemCredentials(gomock.Any(), gomock.Eq([]cbr.CredentialRedemption{{ - Issuer: issuerName, - TokenPreimage: preimage, - Signature: sig, - }}), gomock.Eq(walletID.String())).Return(nil) - */ - - handler := middleware.HTTPSignedOnly(service)(DrainSuggestion(service)) - - drainReq := DrainSuggestionRequest{ - WalletID: walletID, - Credentials: []CredentialBinding{{ - PublicKey: issuerPublicKey, - Signature: sig, - TokenPreimage: preimage, - }}, - } - - body, err := json.Marshal(&drainReq) - suite.Require().NoError(err) - - ctx := context.WithValue(context.Background(), appctx.ReputationOnDrainCTXKey, true) - // this is what happens when we reprocess an errored job which has failed due to duplicate redemption - ctx = context.WithValue(ctx, appctx.SkipRedeemCredentialsCTXKey, true) - - req, err := http.NewRequestWithContext(ctx, "POST", "/suggestion/drain", bytes.NewBuffer(body)) - suite.Require().NoError(err) - - var s httpsignature.SignatureParams - s.Algorithm = httpsignature.ED25519 - s.KeyID = info.ID - s.Headers = []string{"digest", "(request-target)"} - - err = s.Sign(privKey, crypto.Hash(0), req) - suite.Require().NoError(err) - - err = walletDB.InsertWallet(context.Background(), &info) - suite.Require().NoError(err, "Failed to insert wallet") - - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - - suite.Require().Equal(http.StatusBadRequest, rr.Code, "Wallet without payout address should fail") - - req, err = http.NewRequestWithContext(ctx, "POST", "/suggestion/drain", bytes.NewBuffer(body)) - suite.Require().NoError(err) - - err = s.Sign(privKey, crypto.Hash(0), req) - suite.Require().NoError(err) - - info.UserDepositDestination = uuid.NewV4().String() - info.UserDepositAccountProvider = &custodian - - err = walletDB.UpsertWallet(context.Background(), &info) - suite.Require().NoError(err, "Failed to insert wallet") - - rr = httptest.NewRecorder() - handler.ServeHTTP(rr, req) - b, _ := httputil.DumpResponse(rr.Result(), true) - fmt.Printf("%s", b) - suite.Require().Equal(http.StatusOK, rr.Code) - - <-time.After(2 * time.Second) - - var drainJob = getClaimDrainEntry(pg.(*DatastoreWithPrometheus).base.(*Postgres)) - suite.Require().True(drainJob.Erred) - suite.Require().Equal(*drainJob.Status, "reputation-failed", "error code should be reputation-failed") - -} - -func (suite *ControllersTestSuite) TestSuggestionDrainWalletNotReputable() { - // TODO: after we figure out why we are being blocked by bf enable - //suite.T().Skip("bitflyer side unable to settle") - pg, _, err := NewPostgres() - suite.Require().NoError(err, "Failed to get postgres conn") - - walletDB, _, err := wallet.NewPostgres() - suite.Require().NoError(err, "Failed to get postgres conn") - - ch := make(chan *walletutils.TransactionInfo) - - mockCtrl := gomock.NewController(suite.T()) - defer mockCtrl.Finish() - - // setup bf client - var bfClient = mockbitflyer.NewMockClient(mockCtrl) - - publicKey, privKey, err := httpsignature.GenerateEd25519Key(nil) - suite.Require().NoError(err, "Failed to create wallet keypair") - - walletID := uuid.NewV4() - bat := altcurrency.BAT - - custodian := "bitflyer" - - info := walletutils.Info{ - ID: walletID.String(), - Provider: "brave", - ProviderID: "-", - AltCurrency: &bat, - PublicKey: hex.EncodeToString(publicKey), - LastBalance: nil, - } - - mockReputation := mockreputation.NewMockClient(mockCtrl) - // the wallet reputation check originally succeeds - mockReputation.EXPECT().IsWalletReputable( - gomock.Any(), - gomock.Any(), - gomock.Any(), - ).Return( - true, - nil, - ) - // the second batch submitted - mockReputation.EXPECT().IsWalletReputable( - gomock.Any(), - gomock.Any(), - gomock.Any(), - ).Return( - true, - nil, - ) - // the drain reputation check fails - mockReputation.EXPECT().IsWalletAdsReputable( - gomock.Any(), - gomock.Any(), - gomock.Any(), - ).Return( - false, - nil, - ) - // for the second batch - mockReputation.EXPECT().IsWalletAdsReputable( - gomock.Any(), - gomock.Any(), - gomock.Any(), - ).Return( - false, - nil, - ) - mockCB := mockcb.NewMockClient(mockCtrl) - - service := &Service{ - Datastore: pg, - bfClient: bfClient, - cbClient: mockCB, - wallet: &wallet.Service{ - Datastore: walletDB, - }, - reputationClient: mockReputation, - drainChannel: ch, - } - - promotion, err := service.Datastore.CreatePromotion("ads", 2, decimal.NewFromFloat(0.25), "") - suite.Require().NoError(err, "Failed to create promotion") - err = service.Datastore.ActivatePromotion(promotion) - suite.Require().NoError(err, "Failed to activate promotion") - - err = service.wallet.Datastore.UpsertWallet(context.Background(), &info) - suite.Require().NoError(err, "the wallet failed to be inserted") - - claimBonus := 0.25 - grantAmount := decimal.NewFromFloat(0.25) - _, err = service.Datastore.CreateClaim(promotion.ID, info.ID, grantAmount, decimal.NewFromFloat(claimBonus), false) - suite.Require().NoError(err, "create a claim for a promotion") - - issuerName := promotion.ID.String() + ":control" - issuerPublicKey := "dHuiBIasUO0khhXsWgygqpVasZhtQraDSZxzJW2FKQ4=" - blindedCreds := []string{"XhBPMjh4vMw+yoNjE7C5OtoTz2rCtfuOXO/Vk7UwWzY="} - signedCreds := []string{"NJnOyyL6YAKMYo6kSAuvtG+/04zK1VNaD9KdKwuzAjU="} - proof := "IiKqfk10e7SJ54Ud/8FnCf+sLYQzS4WiVtYAM5+RVgApY6B9x4CVbMEngkDifEBRD6szEqnNlc3KA8wokGV5Cw==" - sig := "PsavkSWaqsTzZjmoDBmSu6YxQ7NZVrs2G8DQ+LkW5xOejRF6whTiuUJhr9dJ1KlA+79MDbFeex38X5KlnLzvJw==" - preimage := "125KIuuwtHGEl35cb5q1OLSVepoDTgxfsvwTc7chSYUM2Zr80COP19EuMpRQFju1YISHlnB04XJzZYN2ieT9Ng==" - - mockCB.EXPECT().CreateIssuer(gomock.Any(), gomock.Eq(issuerName), gomock.Eq(defaultMaxTokensPerIssuer)).Return(nil) - mockCB.EXPECT().GetIssuer(gomock.Any(), gomock.Eq(issuerName)).Return(&cbr.IssuerResponse{ - Name: issuerName, - PublicKey: issuerPublicKey, - }, nil) - mockCB.EXPECT().SignCredentials(gomock.Any(), gomock.Eq(issuerName), gomock.Eq(blindedCreds)).Return(&cbr.CredentialsIssueResponse{ - BatchProof: proof, - SignedTokens: signedCreds, - }, nil) - - claimID := suite.ClaimPromotion(service, info, privKey, promotion, blindedCreds, http.StatusOK) - suite.WaitForClaimToPropagate(service, promotion, claimID) - - mockCB.EXPECT().RedeemCredentials(gomock.Any(), gomock.Eq([]cbr.CredentialRedemption{{ - Issuer: issuerName, - TokenPreimage: preimage, - Signature: sig, - }}), gomock.Eq(walletID.String())).Return(nil) - - handler := middleware.HTTPSignedOnly(service)(DrainSuggestion(service)) - - drainReq := DrainSuggestionRequest{ - WalletID: walletID, - Credentials: []CredentialBinding{{ - PublicKey: issuerPublicKey, - Signature: sig, - TokenPreimage: preimage, - }}, - } - - body, err := json.Marshal(&drainReq) - suite.Require().NoError(err) - - ctx := context.WithValue(context.Background(), appctx.ReputationOnDrainCTXKey, true) - req, err := http.NewRequestWithContext(ctx, "POST", "/suggestion/drain", bytes.NewBuffer(body)) - suite.Require().NoError(err) - - var s httpsignature.SignatureParams - s.Algorithm = httpsignature.ED25519 - s.KeyID = info.ID - s.Headers = []string{"digest", "(request-target)"} - - err = s.Sign(privKey, crypto.Hash(0), req) - suite.Require().NoError(err) - - err = walletDB.InsertWallet(context.Background(), &info) - suite.Require().NoError(err, "Failed to insert wallet") - - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - - suite.Require().Equal(http.StatusBadRequest, rr.Code, "Wallet without payout address should fail") - - req, err = http.NewRequestWithContext(ctx, "POST", "/suggestion/drain", bytes.NewBuffer(body)) - suite.Require().NoError(err) - - err = s.Sign(privKey, crypto.Hash(0), req) - suite.Require().NoError(err) - - info.UserDepositDestination = uuid.NewV4().String() - info.UserDepositAccountProvider = &custodian - - err = walletDB.UpsertWallet(context.Background(), &info) - suite.Require().NoError(err, "Failed to insert wallet") - - rr = httptest.NewRecorder() - handler.ServeHTTP(rr, req) - b, _ := httputil.DumpResponse(rr.Result(), true) - fmt.Printf("%s", b) - suite.Require().Equal(http.StatusOK, rr.Code) - - <-time.After(2 * time.Second) - - var drainJob = getClaimDrainEntry(pg.(*DatastoreWithPrometheus).base.(*Postgres)) - suite.Require().True(drainJob.Erred) - suite.Require().Equal(*drainJob.Status, "reputation-failed", "error code should be reputation-failed") - - // testing out the drain info handler - drainInfoHandler := GetCustodianDrainInfo(service) - - req, err = http.NewRequestWithContext(ctx, "GET", "/custodian-drain-info/{paymentId}", bytes.NewBuffer(body)) - suite.Require().NoError(err) - - // setup url param - rctx := chi.NewRouteContext() - rctx.URLParams.Add("paymentId", info.ID) - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) - - rr = httptest.NewRecorder() - drainInfoHandler.ServeHTTP(rr, req) - b, _ = httputil.DumpResponse(rr.Result(), true) - fmt.Printf("%s", b) - suite.Require().Equal(http.StatusOK, rr.Code) - - var resp = CustodianDrainInfoResponse{} - suite.Require().NoError(json.Unmarshal(rr.Body.Bytes(), &resp)) - // make sure there is a drain created by the test - suite.Require().Equal(1, len(resp.Drains)) - // make sure we only have one promition drained - suite.Require().Equal(1, len(resp.Drains[0].PromotionsDrained)) - // check that the values match - suite.Require().True(resp.Drains[0].PromotionsDrained[0].Value.Equal(resp.Drains[0].Value)) - - _, err = pg.RawDB().Exec("delete from claim_creds") - suite.Require().NoError(err, "Failed to get clean table") - _, err = pg.RawDB().Exec("delete from issuers") - suite.Require().NoError(err, "Failed to get clean table") - _, err = pg.RawDB().Exec("delete from claims") - suite.Require().NoError(err, "Failed to get clean table") - _, err = pg.RawDB().Exec("delete from promotions") - suite.Require().NoError(err, "Failed to get clean table") - // perform another drain - - promotion, err = service.Datastore.CreatePromotion("ads", 2, decimal.NewFromFloat(0.25), "") - suite.Require().NoError(err, "Failed to create promotion") - err = service.Datastore.ActivatePromotion(promotion) - suite.Require().NoError(err, "Failed to activate promotion") - - _, err = service.Datastore.CreateClaim(promotion.ID, info.ID, grantAmount, decimal.NewFromFloat(claimBonus), false) - suite.Require().NoError(err, "create a claim for a promotion") - - issuerName = promotion.ID.String() + ":control" - mockCB.EXPECT().CreateIssuer(gomock.Any(), gomock.Eq(issuerName), gomock.Eq(defaultMaxTokensPerIssuer)).Return(nil) - - mockCB.EXPECT().GetIssuer(gomock.Any(), gomock.Eq(issuerName)).Return(&cbr.IssuerResponse{ - Name: issuerName, - PublicKey: issuerPublicKey, - }, nil) - mockCB.EXPECT().SignCredentials(gomock.Any(), gomock.Eq(issuerName), gomock.Eq(blindedCreds)).Return(&cbr.CredentialsIssueResponse{ - BatchProof: proof, - SignedTokens: signedCreds, - }, nil) - - claimID = suite.ClaimPromotion(service, info, privKey, promotion, blindedCreds, http.StatusOK) - suite.WaitForClaimToPropagate(service, promotion, claimID) - - mockCB.EXPECT().RedeemCredentials(gomock.Any(), gomock.Eq([]cbr.CredentialRedemption{{ - Issuer: issuerName, - TokenPreimage: preimage, - Signature: sig, - }}), gomock.Eq(walletID.String())).Return(nil) - - drainReq = DrainSuggestionRequest{ - WalletID: walletID, - Credentials: []CredentialBinding{{ - PublicKey: issuerPublicKey, - Signature: sig, - TokenPreimage: preimage, - }}, - } - - body, err = json.Marshal(&drainReq) - suite.Require().NoError(err) - - req, err = http.NewRequestWithContext(ctx, "POST", "/suggestion/drain", bytes.NewBuffer(body)) - suite.Require().NoError(err) - rr = httptest.NewRecorder() - - err = s.Sign(privKey, crypto.Hash(0), req) - suite.Require().NoError(err) - - handler.ServeHTTP(rr, req) - b, _ = httputil.DumpResponse(rr.Result(), true) - fmt.Printf("%s", b) - suite.Require().Equal(http.StatusOK, rr.Code) - - <-time.After(2 * time.Second) - drainJob = getClaimDrainEntry(pg.(*DatastoreWithPrometheus).base.(*Postgres)) - suite.Require().True(drainJob.Erred) - suite.Require().Equal(*drainJob.Status, "reputation-failed", "error code should be reputation-failed") - - // validate that the batching works - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) - - rr = httptest.NewRecorder() - drainInfoHandler.ServeHTTP(rr, req) - b, _ = httputil.DumpResponse(rr.Result(), true) - fmt.Printf("%s", b) - suite.Require().Equal(http.StatusOK, rr.Code) - - resp = CustodianDrainInfoResponse{} - suite.Require().NoError(json.Unmarshal(rr.Body.Bytes(), &resp)) - // make sure there is a drain created by the test - suite.Require().Equal(2, len(resp.Drains)) - // make sure we only have one promition drained for each batch - suite.Require().Equal(1, len(resp.Drains[0].PromotionsDrained)) - suite.Require().Equal(1, len(resp.Drains[1].PromotionsDrained)) - // check that the sum of each drained promotion matches - suite.Require().True(resp.Drains[0].PromotionsDrained[0].Value.Equal(resp.Drains[0].Value)) - -} - -func (suite *ControllersTestSuite) TestSuggestionDrainBitflyerNoINV() { - // TODO: after we figure out why we are being blocked by bf enable - //suite.T().Skip("bitflyer side unable to settle") - pg, _, err := NewPostgres() - suite.Require().NoError(err, "Failed to get postgres conn") - - walletDB, _, err := wallet.NewPostgres() - suite.Require().NoError(err, "Failed to get postgres conn") - - ch := make(chan *walletutils.TransactionInfo) - - mockCtrl := gomock.NewController(suite.T()) - defer mockCtrl.Finish() - - // setup bf client - var bfClient = mockbitflyer.NewMockClient(mockCtrl) - - priceToken := uuid.NewV4() - JPY := "JPY" - BAT := "BAT" - currencyCode := fmt.Sprintf("%s_%s", BAT, JPY) - - bfClient.EXPECT(). - FetchQuote(gomock.Any(), currencyCode, false). - Return(&bitflyer.Quote{ - PriceToken: priceToken.String(), - ProductCode: currencyCode, - MainCurrency: JPY, - SubCurrency: BAT, - Rate: decimal.New(1, 1), - }, nil) - - withdrawal := bitflyer.WithdrawToDepositIDResponse{ - Status: "NO_INV", - } - - withdrawToDepositIDBulkResponse := bitflyer.WithdrawToDepositIDBulkResponse{ - DryRun: false, - Withdrawals: []bitflyer.WithdrawToDepositIDResponse{ - withdrawal, - }, - } - - bfClient.EXPECT(). - UploadBulkPayout(gomock.Any(), gomock.Any()). - Return(&withdrawToDepositIDBulkResponse, nil) - - publicKey, privKey, err := httpsignature.GenerateEd25519Key(nil) - suite.Require().NoError(err, "Failed to create wallet keypair") - - walletID := uuid.NewV4() - bat := altcurrency.BAT - - custodian := "bitflyer" - - info := walletutils.Info{ - ID: walletID.String(), - Provider: "brave", - ProviderID: "-", - AltCurrency: &bat, - PublicKey: hex.EncodeToString(publicKey), - LastBalance: nil, - } - - mockReputation := mockreputation.NewMockClient(mockCtrl) - mockReputation.EXPECT().IsWalletReputable( - gomock.Any(), - gomock.Any(), - gomock.Any(), - ).Return( - true, - nil, - ) - mockReputation.EXPECT().IsWalletAdsReputable( - gomock.Any(), - gomock.Any(), - gomock.Any(), - ).Return( - true, - nil, - ) - mockCB := mockcb.NewMockClient(mockCtrl) - - service := &Service{ - Datastore: pg, - bfClient: bfClient, - cbClient: mockCB, - wallet: &wallet.Service{ - Datastore: walletDB, - }, - reputationClient: mockReputation, - drainChannel: ch, - } - - promotion, err := service.Datastore.CreatePromotion("ads", 2, decimal.NewFromFloat(0.25), "") - suite.Require().NoError(err, "Failed to create promotion") - err = service.Datastore.ActivatePromotion(promotion) - suite.Require().NoError(err, "Failed to activate promotion") - - err = service.wallet.Datastore.UpsertWallet(context.Background(), &info) - suite.Require().NoError(err, "the wallet failed to be inserted") - - claimBonus := 0.25 - grantAmount := decimal.NewFromFloat(0.25) - _, err = service.Datastore.CreateClaim(promotion.ID, info.ID, grantAmount, decimal.NewFromFloat(claimBonus), false) - suite.Require().NoError(err, "create a claim for a promotion") - - issuerName := promotion.ID.String() + ":control" - issuerPublicKey := "dHuiBIasUO0khhXsWgygqpVasZhtQraDSZxzJW2FKQ4=" - blindedCreds := []string{"XhBPMjh4vMw+yoNjE7C5OtoTz2rCtfuOXO/Vk7UwWzY="} - signedCreds := []string{"NJnOyyL6YAKMYo6kSAuvtG+/04zK1VNaD9KdKwuzAjU="} - proof := "IiKqfk10e7SJ54Ud/8FnCf+sLYQzS4WiVtYAM5+RVgApY6B9x4CVbMEngkDifEBRD6szEqnNlc3KA8wokGV5Cw==" - sig := "PsavkSWaqsTzZjmoDBmSu6YxQ7NZVrs2G8DQ+LkW5xOejRF6whTiuUJhr9dJ1KlA+79MDbFeex38X5KlnLzvJw==" - preimage := "125KIuuwtHGEl35cb5q1OLSVepoDTgxfsvwTc7chSYUM2Zr80COP19EuMpRQFju1YISHlnB04XJzZYN2ieT9Ng==" - - mockCB.EXPECT().CreateIssuer(gomock.Any(), gomock.Eq(issuerName), gomock.Eq(defaultMaxTokensPerIssuer)).Return(nil) - mockCB.EXPECT().GetIssuer(gomock.Any(), gomock.Eq(issuerName)).Return(&cbr.IssuerResponse{ - Name: issuerName, - PublicKey: issuerPublicKey, - }, nil) - mockCB.EXPECT().SignCredentials(gomock.Any(), gomock.Eq(issuerName), gomock.Eq(blindedCreds)).Return(&cbr.CredentialsIssueResponse{ - BatchProof: proof, - SignedTokens: signedCreds, - }, nil) - - claimID := suite.ClaimPromotion(service, info, privKey, promotion, blindedCreds, http.StatusOK) - suite.WaitForClaimToPropagate(service, promotion, claimID) - - mockCB.EXPECT().RedeemCredentials(gomock.Any(), gomock.Eq([]cbr.CredentialRedemption{{ - Issuer: issuerName, - TokenPreimage: preimage, - Signature: sig, - }}), gomock.Eq(walletID.String())).Return(nil) - - handler := middleware.HTTPSignedOnly(service)(DrainSuggestion(service)) - - drainReq := DrainSuggestionRequest{ - WalletID: walletID, - Credentials: []CredentialBinding{{ - PublicKey: issuerPublicKey, - Signature: sig, - TokenPreimage: preimage, - }}, - } - - body, err := json.Marshal(&drainReq) - suite.Require().NoError(err) - - ctx := context.WithValue(context.Background(), appctx.ReputationOnDrainCTXKey, true) - req, err := http.NewRequestWithContext(ctx, "POST", "/suggestion/drain", bytes.NewBuffer(body)) - suite.Require().NoError(err) - - var s httpsignature.SignatureParams - s.Algorithm = httpsignature.ED25519 - s.KeyID = info.ID - s.Headers = []string{"digest", "(request-target)"} - - err = s.Sign(privKey, crypto.Hash(0), req) - suite.Require().NoError(err) - - err = walletDB.InsertWallet(context.Background(), &info) - suite.Require().NoError(err, "Failed to insert wallet") - - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - - suite.Require().Equal(http.StatusBadRequest, rr.Code, "Wallet without payout address should fail") - - req, err = http.NewRequestWithContext(ctx, "POST", "/suggestion/drain", bytes.NewBuffer(body)) - suite.Require().NoError(err) - - err = s.Sign(privKey, crypto.Hash(0), req) - suite.Require().NoError(err) - - info.UserDepositDestination = uuid.NewV4().String() - info.UserDepositAccountProvider = &custodian - - err = walletDB.UpsertWallet(context.Background(), &info) - suite.Require().NoError(err, "Failed to insert wallet") - - rr = httptest.NewRecorder() - handler.ServeHTTP(rr, req) - b, _ := httputil.DumpResponse(rr.Result(), true) - fmt.Printf("%s", b) - suite.Require().Equal(http.StatusOK, rr.Code) - - <-service.drainChannel - <-time.After(1 * time.Second) - - // the run next batch payments job needs to kick in before checking the drain status - attempted, err := service.RunNextBatchPaymentsJob(ctx) - suite.Require().True(attempted) - - var drainJob = getClaimDrainEntry(pg.(*DatastoreWithPrometheus).base.(*Postgres)) - suite.Require().True(drainJob.Erred) - suite.Require().Equal(*drainJob.ErrCode, "bitflyer_no_inv", "error code should be no inv") -} - -func (suite *ControllersTestSuite) TestSuggestionDrainBitflyer() { - // TODO: after we figure out why we are being blocked by bf enable - suite.T().Skip("bitflyer side unable to settle") - pg, _, err := NewPostgres() - suite.Require().NoError(err, "Failed to get postgres conn") - - walletDB, _, err := wallet.NewPostgres() - suite.Require().NoError(err, "Failed to get postgres conn") - - ch := make(chan *walletutils.TransactionInfo) - - mockCtrl := gomock.NewController(suite.T()) - defer mockCtrl.Finish() - - // setup bf client - bfClient, err := bitflyer.New() - suite.Require().NoError(err) - - // setup token in client - payload := bitflyer.TokenPayload{ - GrantType: "client_credentials", - ClientID: os.Getenv("BITFLYER_CLIENT_ID"), - ClientSecret: os.Getenv("BITFLYER_CLIENT_SECRET"), - ExtraClientSecret: os.Getenv("BITFLYER_EXTRA_CLIENT_SECRET"), - } - auth, err := bfClient.RefreshToken( - context.Background(), - payload, - ) - suite.Require().NoError(err) - bfClient.SetAuthToken(auth.AccessToken) - - publicKey, privKey, err := httpsignature.GenerateEd25519Key(nil) - suite.Require().NoError(err, "Failed to create wallet keypair") - - walletID := uuid.NewV4() - bat := altcurrency.BAT - - custodian := "bitflyer" - - info := walletutils.Info{ - ID: walletID.String(), - Provider: "brave", - ProviderID: "-", - AltCurrency: &bat, - PublicKey: hex.EncodeToString(publicKey), - LastBalance: nil, - } - - mockReputation := mockreputation.NewMockClient(mockCtrl) - mockReputation.EXPECT().IsWalletReputable( - gomock.Any(), - gomock.Any(), - gomock.Any(), - ).Return( - true, - nil, - ) - // for draining to work must be reputable - mockReputation.EXPECT().IsWalletAdsReputable( - gomock.Any(), - gomock.Any(), - gomock.Any(), - ).Return( - true, - nil, - ) - mockCB := mockcb.NewMockClient(mockCtrl) - - service := &Service{ - Datastore: pg, - bfClient: bfClient, - cbClient: mockCB, - wallet: &wallet.Service{ - Datastore: walletDB, - }, - reputationClient: mockReputation, - drainChannel: ch, - } - - promotion, err := service.Datastore.CreatePromotion("ads", 2, decimal.NewFromFloat(0.25), "") - suite.Require().NoError(err, "Failed to create promotion") - err = service.Datastore.ActivatePromotion(promotion) - suite.Require().NoError(err, "Failed to activate promotion") - - err = service.wallet.Datastore.UpsertWallet(context.Background(), &info) - suite.Require().NoError(err, "the wallet failed to be inserted") - - claimBonus := 0.25 - grantAmount := decimal.NewFromFloat(0.25) - _, err = service.Datastore.CreateClaim(promotion.ID, info.ID, grantAmount, decimal.NewFromFloat(claimBonus), false) - suite.Require().NoError(err, "create a claim for a promotion") - - issuerName := promotion.ID.String() + ":control" - issuerPublicKey := "dHuiBIasUO0khhXsWgygqpVasZhtQraDSZxzJW2FKQ4=" - blindedCreds := []string{"XhBPMjh4vMw+yoNjE7C5OtoTz2rCtfuOXO/Vk7UwWzY="} - signedCreds := []string{"NJnOyyL6YAKMYo6kSAuvtG+/04zK1VNaD9KdKwuzAjU="} - proof := "IiKqfk10e7SJ54Ud/8FnCf+sLYQzS4WiVtYAM5+RVgApY6B9x4CVbMEngkDifEBRD6szEqnNlc3KA8wokGV5Cw==" - sig := "PsavkSWaqsTzZjmoDBmSu6YxQ7NZVrs2G8DQ+LkW5xOejRF6whTiuUJhr9dJ1KlA+79MDbFeex38X5KlnLzvJw==" - preimage := "125KIuuwtHGEl35cb5q1OLSVepoDTgxfsvwTc7chSYUM2Zr80COP19EuMpRQFju1YISHlnB04XJzZYN2ieT9Ng==" - - mockCB.EXPECT().CreateIssuer(gomock.Any(), gomock.Eq(issuerName), gomock.Eq(defaultMaxTokensPerIssuer)).Return(nil) - mockCB.EXPECT().GetIssuer(gomock.Any(), gomock.Eq(issuerName)).Return(&cbr.IssuerResponse{ - Name: issuerName, - PublicKey: issuerPublicKey, - }, nil) - mockCB.EXPECT().SignCredentials(gomock.Any(), gomock.Eq(issuerName), gomock.Eq(blindedCreds)).Return(&cbr.CredentialsIssueResponse{ - BatchProof: proof, - SignedTokens: signedCreds, - }, nil) - - claimID := suite.ClaimPromotion(service, info, privKey, promotion, blindedCreds, http.StatusOK) - suite.WaitForClaimToPropagate(service, promotion, claimID) - - mockCB.EXPECT().RedeemCredentials(gomock.Any(), gomock.Eq([]cbr.CredentialRedemption{{ - Issuer: issuerName, - TokenPreimage: preimage, - Signature: sig, - }}), gomock.Eq(walletID.String())).Return(nil) - - handler := middleware.HTTPSignedOnly(service)(DrainSuggestion(service)) - - drainReq := DrainSuggestionRequest{ - WalletID: walletID, - Credentials: []CredentialBinding{{ - PublicKey: issuerPublicKey, - Signature: sig, - TokenPreimage: preimage, - }}, - } - - body, err := json.Marshal(&drainReq) - suite.Require().NoError(err) - - ctx := context.WithValue(context.Background(), appctx.ReputationOnDrainCTXKey, true) - req, err := http.NewRequestWithContext(ctx, "POST", "/suggestion/drain", bytes.NewBuffer(body)) - suite.Require().NoError(err) - - var s httpsignature.SignatureParams - s.Algorithm = httpsignature.ED25519 - s.KeyID = info.ID - s.Headers = []string{"digest", "(request-target)"} - - err = s.Sign(privKey, crypto.Hash(0), req) - suite.Require().NoError(err) - - err = walletDB.InsertWallet(context.Background(), &info) - suite.Require().NoError(err, "Failed to insert wallet") - - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - - suite.Require().Equal(http.StatusBadRequest, rr.Code, "Wallet without payout address should fail") - - req, err = http.NewRequestWithContext(ctx, "POST", "/suggestion/drain", bytes.NewBuffer(body)) - suite.Require().NoError(err) - - err = s.Sign(privKey, crypto.Hash(0), req) - suite.Require().NoError(err) - - info.UserDepositDestination = uuid.NewV4().String() - info.UserDepositAccountProvider = &custodian - - err = walletDB.UpsertWallet(context.Background(), &info) - suite.Require().NoError(err, "Failed to insert wallet") - - rr = httptest.NewRecorder() - handler.ServeHTTP(rr, req) - b, _ := httputil.DumpResponse(rr.Result(), true) - fmt.Printf("%s", b) - suite.Require().Equal(http.StatusOK, rr.Code) - - <-ch - - //suite.Require().True(grantAmount.Equals(altcurrency.BAT.FromProbi(tx.Probi))) - - //settlementAddr := os.Getenv("BAT_SETTLEMENT_ADDRESS") - //_, err = w.Transfer(altcurrency.BAT, altcurrency.BAT.ToProbi(grantAmount), settlementAddr) - //suite.Require().NoError(err) -} - -func (suite *ControllersTestSuite) TestSuggestionDrainV2() { - ctx := context.Background() - - pg, _, err := NewPostgres() - suite.Require().NoError(err, "Failed to get postgres conn") - - walletDB, _, err := wallet.NewPostgres() - suite.Require().NoError(err, "Failed to get postgres conn") - - ch := make(chan *walletutils.TransactionInfo) - - mockCtrl := gomock.NewController(suite.T()) - defer mockCtrl.Finish() - - publicKey, privKey, err := httpsignature.GenerateEd25519Key(nil) - suite.Require().NoError(err, "Failed to create wallet keypair") - - walletID := uuid.NewV4() - bat := altcurrency.BAT - info := walletutils.Info{ - ID: walletID.String(), - Provider: "uphold", - ProviderID: "-", - AltCurrency: &bat, - PublicKey: hex.EncodeToString(publicKey), - LastBalance: nil, - } - - w := uphold.Wallet{ - Info: info, - PrivKey: privKey, - PubKey: publicKey, - } - err = w.Register(ctx, "drain-card-test") - suite.Require().NoError(err, "Failed to register wallet") - - mockReputation := mockreputation.NewMockClient(mockCtrl) - mockReputation.EXPECT().IsWalletReputable( - gomock.Any(), - gomock.Any(), - gomock.Any(), - ).Return( - true, - nil, - ) - // for draining to work must be reputable - mockReputation.EXPECT().IsWalletAdsReputable( - gomock.Any(), - gomock.Any(), - gomock.Any(), - ).Return( - true, - nil, - ) - mockCB := mockcb.NewMockClient(mockCtrl) - - service := &Service{ - Datastore: pg, - cbClient: mockCB, - wallet: &wallet.Service{ - Datastore: walletDB, - }, - reputationClient: mockReputation, - drainChannel: ch, - } - - err = service.InitHotWallet(ctx) - suite.Require().NoError(err, "Failed to init hot wallet") - - promotion, err := service.Datastore.CreatePromotion("ads", 2, decimal.NewFromFloat(0.25), "") - suite.Require().NoError(err, "Failed to create promotion") - err = service.Datastore.ActivatePromotion(promotion) - suite.Require().NoError(err, "Failed to activate promotion") - - err = service.wallet.Datastore.UpsertWallet(context.Background(), &info) - suite.Require().NoError(err, "the wallet failed to be inserted") - - claimBonus := 0.25 - grantAmount := decimal.NewFromFloat(0.25) - _, err = service.Datastore.CreateClaim(promotion.ID, info.ID, grantAmount, decimal.NewFromFloat(claimBonus), false) - suite.Require().NoError(err, "create a claim for a promotion") - - issuerName := promotion.ID.String() + ":control" - issuerPublicKey := "dHuiBIasUO0khhXsWgygqpVasZhtQraDSZxzJW2FKQ4=" - blindedCreds := []string{"XhBPMjh4vMw+yoNjE7C5OtoTz2rCtfuOXO/Vk7UwWzY="} - signedCreds := []string{"NJnOyyL6YAKMYo6kSAuvtG+/04zK1VNaD9KdKwuzAjU="} - proof := "IiKqfk10e7SJ54Ud/8FnCf+sLYQzS4WiVtYAM5+RVgApY6B9x4CVbMEngkDifEBRD6szEqnNlc3KA8wokGV5Cw==" - sig := "PsavkSWaqsTzZjmoDBmSu6YxQ7NZVrs2G8DQ+LkW5xOejRF6whTiuUJhr9dJ1KlA+79MDbFeex38X5KlnLzvJw==" - preimage := "125KIuuwtHGEl35cb5q1OLSVepoDTgxfsvwTc7chSYUM2Zr80COP19EuMpRQFju1YISHlnB04XJzZYN2ieT9Ng==" - - mockCB.EXPECT().CreateIssuer(gomock.Any(), gomock.Eq(issuerName), gomock.Eq(defaultMaxTokensPerIssuer)).Return(nil) - mockCB.EXPECT().GetIssuer(gomock.Any(), gomock.Eq(issuerName)).Return(&cbr.IssuerResponse{ - Name: issuerName, - PublicKey: issuerPublicKey, - }, nil) - mockCB.EXPECT().SignCredentials(gomock.Any(), gomock.Eq(issuerName), gomock.Eq(blindedCreds)).Return(&cbr.CredentialsIssueResponse{ - BatchProof: proof, - SignedTokens: signedCreds, - }, nil) - - // calling claim promotion will trigger a RunNextClaimJob - claimID := suite.ClaimPromotion(service, info, privKey, promotion, blindedCreds, http.StatusOK) - suite.WaitForClaimToPropagate(service, promotion, claimID) - - mockCB.EXPECT().RedeemCredentials(gomock.Any(), gomock.Eq([]cbr.CredentialRedemption{{ - Issuer: issuerName, - TokenPreimage: preimage, - Signature: sig, - }}), gomock.Eq(walletID.String())).Return(nil) - - handler := middleware.HTTPSignedOnly(service)(DrainSuggestionV2(service)) - - drainReq := DrainSuggestionRequest{ - WalletID: walletID, - Credentials: []CredentialBinding{{ - PublicKey: issuerPublicKey, - Signature: sig, - TokenPreimage: preimage, - }}, - } - - body, err := json.Marshal(&drainReq) - suite.Require().NoError(err) - - ctx = context.WithValue(ctx, appctx.ReputationOnDrainCTXKey, true) - req, err := http.NewRequestWithContext(ctx, "POST", "/suggestion/drain", bytes.NewBuffer(body)) - suite.Require().NoError(err) - - var s httpsignature.SignatureParams - s.Algorithm = httpsignature.ED25519 - s.KeyID = info.ID - s.Headers = []string{"digest", "(request-target)"} - - err = s.Sign(privKey, crypto.Hash(0), req) - suite.Require().NoError(err) - - err = walletDB.InsertWallet(context.Background(), &w.Info) - suite.Require().NoError(err, "Failed to insert wallet") - - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - - suite.Require().Equal(http.StatusBadRequest, rr.Code, "Wallet without payout address should fail") - - req, err = http.NewRequestWithContext(ctx, "POST", "/suggestion/drain", bytes.NewBuffer(body)) - suite.Require().NoError(err) - - err = s.Sign(privKey, crypto.Hash(0), req) - suite.Require().NoError(err) - - info.UserDepositDestination = w.ProviderID - info.UserDepositAccountProvider = new(string) - *info.UserDepositAccountProvider = "uphold" - - err = walletDB.UpsertWallet(context.Background(), &info) - suite.Require().NoError(err, "Failed to insert wallet") - - rr = httptest.NewRecorder() - handler.ServeHTTP(rr, req) - b, _ := httputil.DumpResponse(rr.Result(), true) - fmt.Printf("%s", b) - - var dsv2r = new(DrainSuggestionV2Response) - - err = json.NewDecoder(rr.Result().Body).Decode(dsv2r) - - suite.Require().NoError(err, "Failed to unmarshal response") - - suite.Require().Equal(http.StatusOK, rr.Code) - - tx := <-ch - suite.Require().True(grantAmount.Equals(altcurrency.BAT.FromProbi(tx.Probi))) - - <-time.After(1 * time.Second) - - settlementAddr := os.Getenv("BAT_SETTLEMENT_ADDRESS") - _, err = w.Transfer(ctx, altcurrency.BAT, altcurrency.BAT.ToProbi(grantAmount), settlementAddr) - suite.Require().NoError(err) - - // pull out drain id, and check the datastore has completed state - drainPoll, err := service.Datastore.GetDrainPoll(dsv2r.DrainID) - suite.Require().NoError(err, "Failed to get drain poll response") - suite.Require().True(drainPoll.Status == "complete") -} - -func (suite *ControllersTestSuite) TestSuggestionDrain() { - ctx := context.Background() - - pg, _, err := NewPostgres() - suite.Require().NoError(err, "Failed to get postgres conn") - - walletDB, _, err := wallet.NewPostgres() - suite.Require().NoError(err, "Failed to get postgres conn") - - ch := make(chan *walletutils.TransactionInfo) - - mockCtrl := gomock.NewController(suite.T()) - defer mockCtrl.Finish() - - publicKey, privKey, err := httpsignature.GenerateEd25519Key(nil) - suite.Require().NoError(err, "Failed to create wallet keypair") - - walletID := uuid.NewV4() - bat := altcurrency.BAT - info := walletutils.Info{ - ID: walletID.String(), - Provider: "uphold", - ProviderID: "-", - AltCurrency: &bat, - PublicKey: hex.EncodeToString(publicKey), - LastBalance: nil, - } - - w := uphold.Wallet{ - Info: info, - PrivKey: privKey, - PubKey: publicKey, - } - err = w.Register(ctx, "drain-card-test") - suite.Require().NoError(err, "Failed to register wallet") - - mockReputation := mockreputation.NewMockClient(mockCtrl) - mockReputation.EXPECT().IsWalletReputable( - gomock.Any(), - gomock.Any(), - gomock.Any(), - ).Return( - true, - nil, - ) - mockReputation.EXPECT().IsWalletAdsReputable( - gomock.Any(), - gomock.Any(), - gomock.Any(), - ).Return( - true, - nil, - ) - mockCB := mockcb.NewMockClient(mockCtrl) - - service := &Service{ - Datastore: pg, - cbClient: mockCB, - wallet: &wallet.Service{ - Datastore: walletDB, - }, - reputationClient: mockReputation, - drainChannel: ch, - } - - err = service.InitHotWallet(ctx) - suite.Require().NoError(err, "Failed to init hot wallet") - - promotion, err := service.Datastore.CreatePromotion("ads", 2, decimal.NewFromFloat(0.25), "") - suite.Require().NoError(err, "Failed to create promotion") - err = service.Datastore.ActivatePromotion(promotion) - suite.Require().NoError(err, "Failed to activate promotion") - - err = service.wallet.Datastore.UpsertWallet(context.Background(), &info) - suite.Require().NoError(err, "the wallet failed to be inserted") - - claimBonus := 0.25 - grantAmount := decimal.NewFromFloat(0.25) - _, err = service.Datastore.CreateClaim(promotion.ID, info.ID, grantAmount, decimal.NewFromFloat(claimBonus), false) - suite.Require().NoError(err, "create a claim for a promotion") - - issuerName := promotion.ID.String() + ":control" - issuerPublicKey := "dHuiBIasUO0khhXsWgygqpVasZhtQraDSZxzJW2FKQ4=" - blindedCreds := []string{"XhBPMjh4vMw+yoNjE7C5OtoTz2rCtfuOXO/Vk7UwWzY="} - signedCreds := []string{"NJnOyyL6YAKMYo6kSAuvtG+/04zK1VNaD9KdKwuzAjU="} - proof := "IiKqfk10e7SJ54Ud/8FnCf+sLYQzS4WiVtYAM5+RVgApY6B9x4CVbMEngkDifEBRD6szEqnNlc3KA8wokGV5Cw==" - sig := "PsavkSWaqsTzZjmoDBmSu6YxQ7NZVrs2G8DQ+LkW5xOejRF6whTiuUJhr9dJ1KlA+79MDbFeex38X5KlnLzvJw==" - preimage := "125KIuuwtHGEl35cb5q1OLSVepoDTgxfsvwTc7chSYUM2Zr80COP19EuMpRQFju1YISHlnB04XJzZYN2ieT9Ng==" - - mockCB.EXPECT().CreateIssuer(gomock.Any(), gomock.Eq(issuerName), gomock.Eq(defaultMaxTokensPerIssuer)).Return(nil) - mockCB.EXPECT().GetIssuer(gomock.Any(), gomock.Eq(issuerName)).Return(&cbr.IssuerResponse{ - Name: issuerName, - PublicKey: issuerPublicKey, - }, nil) - mockCB.EXPECT().SignCredentials(gomock.Any(), gomock.Eq(issuerName), gomock.Eq(blindedCreds)).Return(&cbr.CredentialsIssueResponse{ - BatchProof: proof, - SignedTokens: signedCreds, - }, nil) - - claimID := suite.ClaimPromotion(service, info, privKey, promotion, blindedCreds, http.StatusOK) - suite.WaitForClaimToPropagate(service, promotion, claimID) - - mockCB.EXPECT().RedeemCredentials(gomock.Any(), gomock.Eq([]cbr.CredentialRedemption{{ - Issuer: issuerName, - TokenPreimage: preimage, - Signature: sig, - }}), gomock.Eq(walletID.String())).Return(nil) - - handler := middleware.HTTPSignedOnly(service)(DrainSuggestion(service)) - - drainReq := DrainSuggestionRequest{ - WalletID: walletID, - Credentials: []CredentialBinding{{ - PublicKey: issuerPublicKey, - Signature: sig, - TokenPreimage: preimage, - }}, - } - - body, err := json.Marshal(&drainReq) - suite.Require().NoError(err) - - ctx = context.WithValue(ctx, appctx.ReputationOnDrainCTXKey, true) - req, err := http.NewRequestWithContext(ctx, "POST", "/suggestion/drain", bytes.NewBuffer(body)) - suite.Require().NoError(err) - - var s httpsignature.SignatureParams - s.Algorithm = httpsignature.ED25519 - s.KeyID = info.ID - s.Headers = []string{"digest", "(request-target)"} - - err = s.Sign(privKey, crypto.Hash(0), req) - suite.Require().NoError(err) - - err = walletDB.InsertWallet(context.Background(), &w.Info) - suite.Require().NoError(err, "Failed to insert wallet") - - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - - suite.Require().Equal(http.StatusBadRequest, rr.Code, "Wallet without payout address should fail") - - req, err = http.NewRequestWithContext(ctx, "POST", "/suggestion/drain", bytes.NewBuffer(body)) - suite.Require().NoError(err) - - err = s.Sign(privKey, crypto.Hash(0), req) - suite.Require().NoError(err) - - info.UserDepositDestination = w.ProviderID - info.UserDepositAccountProvider = new(string) - *info.UserDepositAccountProvider = "uphold" - - err = walletDB.UpsertWallet(context.Background(), &info) - suite.Require().NoError(err, "Failed to insert wallet") - - rr = httptest.NewRecorder() - handler.ServeHTTP(rr, req) - b, _ := httputil.DumpResponse(rr.Result(), true) - fmt.Printf("%s", b) - suite.Require().Equal(http.StatusOK, rr.Code) - - tx := <-ch - suite.Require().True(grantAmount.Equals(altcurrency.BAT.FromProbi(tx.Probi))) - - <-time.After(1 * time.Second) - settlementAddr := os.Getenv("BAT_SETTLEMENT_ADDRESS") - _, err = w.Transfer(ctx, altcurrency.BAT, altcurrency.BAT.ToProbi(grantAmount), settlementAddr) - suite.Require().NoError(err) - - // testing out the drain info handler - drainInfoHandler := GetCustodianDrainInfo(service) - - req, err = http.NewRequestWithContext(ctx, "GET", "/suggestion/drain", bytes.NewBuffer(body)) - suite.Require().NoError(err) - - // setup url param - rctx := chi.NewRouteContext() - rctx.URLParams.Add("paymentId", info.ID) - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) - - rr = httptest.NewRecorder() - drainInfoHandler.ServeHTTP(rr, req) - b, _ = httputil.DumpResponse(rr.Result(), true) - fmt.Printf("%s", b) - suite.Require().Equal(http.StatusOK, rr.Code) -} - -// THIS CODE IS A QUICK AND DIRTY HACK -// WE SHOULD DELETE ALL OF THIS AND MOVE OVER TO THE PAYMENT SERVICE ONCE DEMO IS DONE. - -// CreateOrder creates orders given the total price, merchant ID, status and items of the order -func (suite *ControllersTestSuite) CreateOrder() (string, error) { - pg, _, err := NewPostgres() - tx := pg.RawDB().MustBegin() - defer pg.RollbackTx(tx) + tx := pg.RawDB().MustBegin() + defer pg.RollbackTx(tx) var id string @@ -3213,122 +1286,3 @@ func (suite *ControllersTestSuite) TestPostReportBAPEvent() { suite.Require().JSONEq(string(serializedExpected1), string(serializedActual1)) } - -func (suite *ControllersTestSuite) TestPatchDrainJobErred_Success() { - pg, _, err := NewPostgres() - suite.Require().NoError(err) - - walletID := uuid.NewV4() - - query := `INSERT INTO claim_drain (wallet_id, erred, errcode, status, batch_id, credentials, completed, total) - VALUES ($1, $2, $3, $4, $5, '[{"t":"123"}]', FALSE, 1);` - - _, err = pg.RawDB().ExecContext(context.Background(), query, walletID, true, "some-failed-errcode", "reputation-failed", - uuid.NewV4().String()) - suite.Require().NoError(err, "should have inserted claim drain row") - - service := &Service{Datastore: pg} - - router := chi.NewRouter() - router.Method("PATCH", "/drain-jobs/wallets/{walletId}/erred", PatchDrainJobErred(service)) - - rw := httptest.NewRecorder() - - data := DrainJobRequest{ - Erred: false, - } - - payload, err := json.Marshal(data) - suite.Require().NoError(err, "should serialize data") - - req := httptest.NewRequest(http.MethodPatch, fmt.Sprintf("/drain-jobs/wallets/%s/erred", walletID), bytes.NewReader(payload)) - - server := &http.Server{Addr: ":8080", Handler: router} - server.Handler.ServeHTTP(rw, req) - - suite.Require().Equal(http.StatusNoContent, rw.Code) - - var drainJob DrainJob - err = pg.RawDB().Get(&drainJob, `SELECT * FROM claim_drain WHERE wallet_id = $1 LIMIT 1`, walletID) - suite.Require().NoError(err, "should have retrieved drain job") - - suite.Require().Equal(walletID, drainJob.WalletID) - suite.Require().Equal(false, drainJob.Erred) - suite.Require().Equal("manual-retry", *drainJob.Status) -} - -func (suite *ControllersTestSuite) TestPatchDrainJobErred_NotFound() { - pg, _, err := NewPostgres() - suite.Require().NoError(err) - - service := &Service{Datastore: pg} - - router := chi.NewRouter() - router.Method("PATCH", "/drain-jobs/wallets/{walletId}/erred", PatchDrainJobErred(service)) - - rw := httptest.NewRecorder() - - data := DrainJobRequest{ - Erred: false, - } - - walletID := uuid.NewV4() - - payload, err := json.Marshal(data) - suite.Require().NoError(err, "should serialize data") - - req := httptest.NewRequest(http.MethodPatch, fmt.Sprintf("/drain-jobs/wallets/%s/erred", walletID), - bytes.NewReader(payload)) - - server := &http.Server{Addr: ":8080", Handler: router} - server.Handler.ServeHTTP(rw, req) - - expected := handlers.AppError{ - Cause: nil, - Message: fmt.Sprintf("patch drain job: no updateable drain job found for walletId %s", walletID), - Code: http.StatusNotFound, - } - - var appError handlers.AppError - err = json.NewDecoder(rw.Body).Decode(&appError) - - suite.Require().Equal(http.StatusNotFound, rw.Code) - suite.Require().Equal(expected, appError) -} - -func (suite *ControllersTestSuite) TestPatchDrainJobErred_ValidationError_Erred() { - pg, _, err := NewPostgres() - suite.Require().NoError(err) - - service := &Service{Datastore: pg} - - router := chi.NewRouter() - router.Method("PATCH", "/drain-jobs/wallets/{walletId}/erred", PatchDrainJobErred(service)) - - rw := httptest.NewRecorder() - - drainJobRequest := DrainJobRequest{ - Erred: true, - } - - payload, err := json.Marshal(drainJobRequest) - suite.Require().NoError(err, "should serialize data") - - req := httptest.NewRequest(http.MethodPatch, fmt.Sprintf("/drain-jobs/wallets/%s/erred", uuid.NewV4()), - bytes.NewReader(payload)) - - server := &http.Server{Addr: ":8080", Handler: router} - server.Handler.ServeHTTP(rw, req) - - expected := "invalid value true only false is supported" - - var appError handlers.AppError - err = json.NewDecoder(rw.Body).Decode(&appError) - - data := appError.Data.(map[string]interface{}) - actual := data["validationErrors"].(map[string]interface{}) - - suite.Require().Equal(http.StatusBadRequest, rw.Code) - suite.Require().Equal(http.StatusBadRequest, appError.Code) - suite.Require().Equal(expected, actual["erred"]) -} diff --git a/services/promotion/datastore.go b/services/promotion/datastore.go index 8cbf28c80..cd669b877 100644 --- a/services/promotion/datastore.go +++ b/services/promotion/datastore.go @@ -10,20 +10,14 @@ import ( "strings" "time" - "github.com/jmoiron/sqlx" "github.com/prometheus/client_golang/prometheus" - "github.com/brave-intl/bat-go/libs/clients/gemini" - "github.com/brave-intl/bat-go/libs/custodian" - "github.com/brave-intl/bat-go/libs/ptr" - "github.com/brave-intl/bat-go/libs/clients" "github.com/brave-intl/bat-go/libs/clients/cbr" appctx "github.com/brave-intl/bat-go/libs/context" "github.com/brave-intl/bat-go/libs/datastore" errorutils "github.com/brave-intl/bat-go/libs/errors" "github.com/brave-intl/bat-go/libs/jsonutils" - "github.com/brave-intl/bat-go/libs/logging" walletutils "github.com/brave-intl/bat-go/libs/wallet" "github.com/getsentry/sentry-go" "github.com/lib/pq" @@ -76,16 +70,6 @@ type BATLossEvent struct { Platform string `db:"platform" json:"platform"` } -// DrainClaim holds drain claim data -type DrainClaim struct { - BatchID *uuid.UUID - Claim *Claim - Credentials []cbr.CredentialRedemption - Wallet *walletutils.Info - Total decimal.Decimal - CodedErr errorutils.DrainCodified -} - // Datastore abstracts over the underlying datastore type Datastore interface { datastore.Datastore @@ -138,22 +122,6 @@ type Datastore interface { InsertBATLossEvent(ctx context.Context, paymentID uuid.UUID, reportID int, amount decimal.Decimal, platform string) (bool, error) // InsertBAPReportEvent inserts a BAP report InsertBAPReportEvent(ctx context.Context, paymentID uuid.UUID, amount decimal.Decimal) (*uuid.UUID, error) - // DrainClaim by marking the claim as drained and inserting a new drain entry - DrainClaim(drainID *uuid.UUID, claim *Claim, credentials []cbr.CredentialRedemption, wallet *walletutils.Info, total decimal.Decimal, codedErr errorutils.DrainCodified) error - // InsertBatchDrainClaim insert drain claims - DrainClaims(drainClaims []DrainClaim) error - // RunNextDrainJob to process deposits if there is one waiting - RunNextDrainJob(ctx context.Context, worker DrainWorker) (bool, error) - // RunNextDrainRetryJob toggles failed drain jobs to be reprocessed if eligible - RunNextDrainRetryJob(ctx context.Context, worker DrainRetryWorker) error - // EnqueueMintDrainJob - enqueue a mint drain job in "pending" status - EnqueueMintDrainJob(ctx context.Context, walletID uuid.UUID, promotionIDs ...uuid.UUID) error - // SetMintDrainPromotionTotal - set the per promotion total for the mint drain - SetMintDrainPromotionTotal(ctx context.Context, walletID, promotionID uuid.UUID, total decimal.Decimal) error - // RunNextMintDrainJob to create new grants from the mint queue - RunNextMintDrainJob(ctx context.Context, worker MintWorker) (bool, error) - // RunNextGeminiCheckStatus periodically check the status of gemini claim drain transactions - RunNextGeminiCheckStatus(ctx context.Context, worker GeminiTxnStatusWorker) (bool, error) // Remove once this is completed https://github.com/brave-intl/bat-go/issues/263 @@ -165,16 +133,6 @@ type Datastore interface { CreateTransaction(orderID uuid.UUID, externalTransactionID string, status string, currency string, kind string, amount decimal.Decimal) (*Transaction, error) // GetSumForTransactions gets a decimal sum of for transactions for an order GetSumForTransactions(orderID uuid.UUID) (decimal.Decimal, error) - // GetDrainPoll gets the information about a drain poll job - GetDrainPoll(drainID *uuid.UUID) (*DrainPoll, error) - // GetDrainsByBatchID gets the information about a drain poll job - GetDrainsByBatchID(ctx context.Context, batchID *uuid.UUID) ([]DrainTransfer, error) - // GetCustodianDrainInfo gets the information about a drain poll job - GetCustodianDrainInfo(paymentID *uuid.UUID) ([]CustodianDrain, error) - // RunNextBatchPaymentsJob to sign claim credentials if there is a claim waiting - RunNextBatchPaymentsJob(ctx context.Context, worker BatchTransferWorker) (bool, error) - // UpdateDrainJobErred - manually update drain job for retry - UpdateDrainJobAsRetriable(ctx context.Context, walletID uuid.UUID) error } // ReadOnlyDatastore includes all database methods that can be made with a read only db connection @@ -203,12 +161,6 @@ type ReadOnlyDatastore interface { // GetClaimByWalletAndPromotion gets whether a wallet has a claimed grants // with the given promotion and returns the grant if so GetClaimByWalletAndPromotion(wallet *walletutils.Info, promotionID *Promotion) (*Claim, error) - // GetDrainPoll gets the information about a drain poll job - GetDrainPoll(drainID *uuid.UUID) (*DrainPoll, error) - // GetCustodianDrainInfo gets the information about a drain poll job - GetCustodianDrainInfo(paymentID *uuid.UUID) ([]CustodianDrain, error) - // GetDrainsByBatchID gets the information about a drain poll job - GetDrainsByBatchID(ctx context.Context, batchID *uuid.UUID) ([]DrainTransfer, error) } // Postgres is a Datastore wrapper around a postgres database @@ -794,181 +746,6 @@ func (pg *Postgres) SaveClaimCreds(creds *ClaimCreds) error { return err } -// MarkBatchTransferSubmitted mark this batch of transfers submitted -func (pg *Postgres) MarkBatchTransferSubmitted(ctx context.Context, batchID *uuid.UUID) error { - tx, err := pg.RawDB().BeginTxx(ctx, nil) - if err != nil { - return err - } - defer pg.RollbackTx(tx) - - stmt := "update claim_drain set status = 'complete', completed=true, completed_at = now() where batch_id = $1" - if _, err := tx.Exec(stmt, batchID); err == nil { - return tx.Commit() - } - return fmt.Errorf("failed to mark batch transfer submitted: %w", err) -} - -// GetCustodianDrainInfo Get the status of the custodian drain info -func (pg *Postgres) GetCustodianDrainInfo(paymentID *uuid.UUID) ([]CustodianDrain, error) { - resp := []CustodianDrain{} - // get the linked wallet info - stmt := ` -select - user_deposit_account_provider, user_deposit_destination -from - wallets -where - id = $1 -` - var custodian Custodian - if err := pg.RawDB().Get(&custodian, stmt, paymentID); err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, nil - } - return nil, err - } - - // get all of the drain jobs for this payment id - stmt = ` -select - batch_id, - split_part(credentials->0->>'issuer',':',1) as promotion_id, - completed_at, - json_array_length(credentials)*0.25 as value, - case when erred then 'errored' else 'succeeded' end as state, - errcode, - transaction_id -from - claim_drain -where - wallet_id = $1 -` - type batchedPromotionsDrained struct { - DrainInfo - BatchID uuid.UUID `db:"batch_id"` - } - - var promosDrained = []batchedPromotionsDrained{} - if err := pg.RawDB().Select(&promosDrained, stmt, paymentID); err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, nil - } - return nil, err - } - - batches := map[uuid.UUID][]DrainInfo{} - batchValue := map[uuid.UUID]decimal.Decimal{} - - // chunk all these into related batches - for i := 0; i < len(promosDrained); i++ { - if _, ok := batches[promosDrained[i].BatchID]; !ok { - batches[promosDrained[i].BatchID] = []DrainInfo{} - } - if _, ok := batchValue[promosDrained[i].BatchID]; !ok { - batchValue[promosDrained[i].BatchID] = decimal.Zero - } - batches[promosDrained[i].BatchID] = append( - batches[promosDrained[i].BatchID], - DrainInfo{ - PromotionID: promosDrained[i].PromotionID, - TransactionID: promosDrained[i].TransactionID, - CompletedAt: promosDrained[i].CompletedAt, - State: promosDrained[i].State, - ErrCode: promosDrained[i].ErrCode, - Value: promosDrained[i].Value, - }, - ) - batchValue[promosDrained[i].BatchID] = batchValue[promosDrained[i].BatchID].Add(promosDrained[i].Value) - } - - // for each batch go through and create a custodian drain and add to resp drain - // add values along the way - for k := range batches { - resp = append(resp, CustodianDrain{ - BatchID: k, - Custodian: custodian, - PromotionsDrained: batches[k], - Value: batchValue[k], - }) - } - - return resp, nil -} - -// GetDrainPoll Get the status of the drain poll job -func (pg *Postgres) GetDrainPoll(drainID *uuid.UUID) (*DrainPoll, error) { - type dbDrainPoll struct { - ID *uuid.UUID `db:"batch_id"` - Completed bool `db:"completed"` - Pending bool `db:"pending"` - Delayed bool `db:"delayed"` - InProgress bool `db:"inprogress"` - } - var ( - drainPoll = new(dbDrainPoll) - err error - ) - - statement := ` -select - batch_id, - bool_and(completed) as completed, - bool_or(erred) as delayed, - (not bool_and(completed) and not bool_or(erred)) as inprogress, - (not bool_or(completed)) as pending -from - claim_drain -where - batch_id = $1 -group by - batch_id` - - err = pg.RawDB().Get(drainPoll, statement, drainID) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return &DrainPoll{ - ID: drainID, - Status: "unknown", - }, nil - } - return nil, err - } - - if drainPoll.Completed { - return &DrainPoll{ - ID: drainID, - Status: "complete", - }, nil - } - - if drainPoll.Delayed { - return &DrainPoll{ - ID: drainID, - Status: "delayed", - }, nil - } - - if drainPoll.Pending { - return &DrainPoll{ - ID: drainID, - Status: "pending", - }, nil - } - - if drainPoll.InProgress { - return &DrainPoll{ - ID: drainID, - Status: "in_progress", - }, nil - } - - return &DrainPoll{ - ID: drainID, - Status: "unknown", - }, nil -} - // GetClaimSummary aggregates the values of a single wallet's claims func (pg *Postgres) GetClaimSummary(walletID uuid.UUID, grantType string) (*ClaimSummary, error) { statement := ` @@ -1031,103 +808,6 @@ ORDER BY created_at DESC return nil, nil } -// RunNextBatchPaymentsJob to sign claim credentials if there is a claim waiting, returning true if a job was attempted -func (pg *Postgres) RunNextBatchPaymentsJob(ctx context.Context, worker BatchTransferWorker) (bool, error) { - // setup a logger - logger := logging.Logger(ctx, "promotion.RunNextBatchPaymentsJob") - // create a tx - tx, err := pg.RawDB().Beginx() - attempted := false - if err != nil { - return attempted, err - } - defer pg.RollbackTx(tx) - - // first get a lock on the batch id, - // only for batches that are all "pending" and have transaction_ids - - statement := ` - select - cd.batch_id - from - claim_drain cd - join wallets w on w.id=cd.wallet_id - where - cd.erred = false and - w.user_deposit_account_provider = 'bitflyer' and - cd.batch_id in (select distinct batch_id from claim_drain where status='prepared') - group by - cd.batch_id - having bool_and(transaction_id is not null) = true - and bool_and(cd.status = 'prepared') = true - limit 1 -` - var batchID = new(uuid.UUID) - - err = tx.Get(batchID, statement) - if err != nil { - // no claims to process - if errors.Is(err, sql.ErrNoRows) { - return attempted, nil - } - return attempted, fmt.Errorf("batch payment job: sql error %w", err) - } - attempted = true - - // put a lock on the batch so it is not picked up - query := "SELECT pg_advisory_xact_lock(hashtext($1))" - _, err = tx.ExecContext(ctx, query, batchID.String()) - if err != nil { - return false, fmt.Errorf("failed to acquire tx lock for batch id %s: %w", batchID.String(), err) - } - - // perform submit against payments API - err = worker.SubmitBatchTransfer(ctx, batchID) - if err != nil { - - var eb *errorutils.ErrorBundle - if errors.As(err, &eb) { - logger.Error(). - Str("error bundle", eb.DataToString()). - Msg("failed to submit batch transfers: error bundle") - } - logger.Error().Err(err).Msg("failed to submit batch transfers") - - status, errCode, _ := errToDrainCode(err) - sentry.CaptureException(fmt.Errorf("errCode: %s - %w", errCode, err)) - countClaimDrainStatus.With(prometheus.Labels{"custodian": "bitflyer", "status": "failed"}).Inc() - - stmt := "update claim_drain set erred = true, errcode = $1, status = $2 where batch_id = $3" - if _, err := tx.Exec(stmt, errCode, status, batchID); err != nil { - return attempted, err - } - - if err := tx.Commit(); err != nil { - return attempted, err - } - - return attempted, err - } - - _, err = tx.Exec(` - update claim_drain set status = 'submitted' - where batch_id = $1 and - erred = false and - transaction_id is not null`, batchID) - if err != nil { - return attempted, err - } - - err = tx.Commit() - if err != nil { - return attempted, err - } - - countClaimDrainStatus.With(prometheus.Labels{"custodian": "bitflyer", "status": "complete"}).Inc() - - return attempted, nil -} - // RunNextClaimJob to sign claim credentials if there is a claim waiting, returning true if a job was attempted func (pg *Postgres) RunNextClaimJob(ctx context.Context, worker ClaimWorker) (bool, error) { tx, err := pg.RawDB().Beginx() @@ -1333,153 +1013,99 @@ func (pg *Postgres) GetOrder(orderID uuid.UUID) (*Order, error) { return &order, nil } -// SetMintDrainPromotionTotal - set the total number of redemptions for this drain job -func (pg *Postgres) SetMintDrainPromotionTotal(ctx context.Context, walletID, promotionID uuid.UUID, total decimal.Decimal) error { - - statement := ` -update mint_drain_promotion set total = $1, done = true where -mint_drain_id=(select id from mint_drain where wallet_id=$2) and -promotion_id=$3` +// UpdateOrder updates the orders status. +// +// Status should either be one of pending, paid, fulfilled, or canceled. +func (pg *Postgres) UpdateOrder(orderID uuid.UUID, status string) error { + result, err := pg.RawDB().Exec(`UPDATE orders set status = $1, updated_at = CURRENT_TIMESTAMP where id = $2`, status, orderID) - _, err := pg.Exec(statement, total, walletID, promotionID) if err != nil { return err } + rowsAffected, err := result.RowsAffected() + if rowsAffected == 0 || err != nil { + return errors.New("no rows updated") + } + return nil } -// EnqueueMintDrainJob - enqueue a mint drain job in "pending" status -func (pg *Postgres) EnqueueMintDrainJob(ctx context.Context, walletID uuid.UUID, promotionIDs ...uuid.UUID) error { - tx, err := pg.RawDB().Beginx() - if err != nil { - return err - } +// CreateTransaction creates a transaction given an orderID, externalTransactionID, currency, and a kind of transaction +func (pg *Postgres) CreateTransaction(orderID uuid.UUID, externalTransactionID string, status string, currency string, kind string, amount decimal.Decimal) (*Transaction, error) { + tx := pg.RawDB().MustBegin() defer pg.RollbackTx(tx) - var mintDrainJob = MintDrainJob{} + var transaction Transaction + err := tx.Get(&transaction, + ` + INSERT INTO transactions (order_id, external_transaction_id, status, currency, kind, amount) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING * + `, orderID, externalTransactionID, status, currency, kind, amount) - statement := ` - insert into mint_drain (wallet_id) - values ($1) - returning *` - err = tx.GetContext(ctx, &mintDrainJob, statement, walletID) if err != nil { - return err - } - - for _, id := range promotionIDs { - _, err = tx.Exec(` - insert into mint_drain_promotion - (mint_drain_id, promotion_id) - values - ($1, $2)`, mintDrainJob.ID, id) - if err != nil { - return err - } + return nil, err } err = tx.Commit() if err != nil { - return err + return nil, err } - return nil + return &transaction, nil } -// DrainClaim by marking the claim as drained and inserting a new drain entry -func (pg *Postgres) DrainClaim(batchID *uuid.UUID, claim *Claim, credentials []cbr.CredentialRedemption, wallet *walletutils.Info, total decimal.Decimal, codedErr errorutils.DrainCodified) error { - tx, err := pg.RawDB().Beginx() - if err != nil { - return err - } - defer pg.RollbackTx(tx) - - err = pg.txDrainClaim(tx, batchID, claim, credentials, wallet, total, codedErr) - if err != nil { - return fmt.Errorf("drain claim: error for claimID %s: %w", claim.ID, err) - } +// GetSumForTransactions returns the calculated sum +func (pg *Postgres) GetSumForTransactions(orderID uuid.UUID) (decimal.Decimal, error) { + var sum decimal.Decimal - err = tx.Commit() - if err != nil { - return err - } + err := pg.RawDB().Get(&sum, ` + SELECT SUM(amount) as sum + FROM transactions + WHERE order_id = $1 AND status = 'completed' + `, orderID) - return nil + return sum, err } -// DrainClaims marks all drain claim as drained and inserts a new drain entry -func (pg *Postgres) DrainClaims(drainClaims []DrainClaim) error { - tx, err := pg.RawDB().Beginx() +// UpdateDrainJobAsRetriable - updates a drain job as retriable +func (pg *Postgres) UpdateDrainJobAsRetriable(ctx context.Context, walletID uuid.UUID) error { + query := ` + UPDATE claim_drain + SET erred = FALSE, status = 'manual-retry' + WHERE wallet_id = $1 AND erred = TRUE AND status IN ('reputation-failed', 'failed') AND transaction_id IS NULL + ` + result, err := pg.ExecContext(ctx, query, walletID) if err != nil { - return fmt.Errorf("insert batch drain claim: error could not begin tx: %w", err) + return fmt.Errorf("update drain job: failed to exec update for walletID %s: %w", walletID, err) } - defer pg.RollbackTx(tx) - for _, d := range drainClaims { - err = pg.txDrainClaim(tx, d.BatchID, d.Claim, d.Credentials, d.Wallet, d.Total, d.CodedErr) - if err != nil { - return fmt.Errorf("insert batch drain claim: error could not insert drain claim for claimID %s: %w", - d.Claim.ID, err) - } + affectedRows, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("update drain job: failed to get affected rows for walletID %s: %w", walletID, err) } - err = tx.Commit() - if err != nil { - return fmt.Errorf("insert batch drain claim: error could not commit drain claims: %w", err) + if affectedRows == 0 { + return fmt.Errorf("update drain job: failed to update row for walletID %s: %w", walletID, + errorutils.ErrNotFound) } return nil } -func (pg *Postgres) txDrainClaim(tx *sqlx.Tx, batchID *uuid.UUID, claim *Claim, credentials []cbr.CredentialRedemption, - wallet *walletutils.Info, total decimal.Decimal, codedErr errorutils.DrainCodified) error { - - credentialsJSON, err := json.Marshal(credentials) - if err != nil { - return err - } - - var claimID *uuid.UUID - // if the claim is not nil, we should set it to drained, as we are in drained state - // this often happens when the wallet is mismatched - if claim != nil { - _, err = tx.Exec(`update claims set drained = true, drained_at = now() where id = $1 and not drained`, claim.ID) - if err != nil { - return fmt.Errorf("failed to set claim as drained: %w", err) - } - claimID = &claim.ID - } else { - claimID = nil - } - - var claimDrain = DrainJob{} - - if codedErr == nil { - statement := ` - insert into claim_drain (credentials, wallet_id, total, batch_id, claim_id, deposit_destination, updated_at) - values ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP) - returning *` - err = tx.Get(&claimDrain, statement, credentialsJSON, wallet.ID, total, batchID, claim.ID, &wallet.UserDepositDestination) - if err != nil { - return err - } - } else { - code, _ := codedErr.DrainCode() - - // insert errored claim drain item - statement := ` - insert into claim_drain (credentials, wallet_id, total, batch_id, claim_id, deposit_destination, erred, errcode, updated_at) - values ($1, $2, $3, $4, $5, $6, true, $7, CURRENT_TIMESTAMP) - returning *` - err = tx.Get(&claimDrain, statement, credentialsJSON, wallet.ID, total, - batchID, claimID, &wallet.UserDepositDestination, code) +func toUUIDs(a ...string) ([]uuid.UUID, error) { + var ( + b = []uuid.UUID{} + ) + for _, id := range a { + v, err := uuid.FromString(id) if err != nil { - return fmt.Errorf("failed to insert erred drain job: %w", err) + return nil, err } + b = append(b, v) } - - return nil + return b, nil } // errToDrainCode - given a drain related processing error, generate a code and retriable flag @@ -1547,522 +1173,3 @@ func errToDrainCode(err error) (string, string, bool) { } return status, strings.ToLower(errCode), retriable } - -// DrainJob - definition of a drain job -type DrainJob struct { - ID uuid.UUID `db:"id"` - ClaimID *uuid.UUID `db:"claim_id"` - Credentials string `db:"credentials"` - WalletID uuid.UUID `db:"wallet_id"` - Total decimal.Decimal `db:"total"` - TransactionID *string `db:"transaction_id"` - Erred bool `db:"erred"` - ErrCode *string `db:"errcode"` - Status *string `db:"status"` - BatchID *uuid.UUID `db:"batch_id"` - Completed bool `db:"completed"` - CompletedAt pq.NullTime `db:"completed_at"` - UpdatedAt pq.NullTime `db:"updated_at"` - DepositDestination *string `db:"deposit_destination"` -} - -var txStatusToStatus = map[string]string{ - "bitflyer-consolidate": "prepared", - txnStatusGeminiPending: txnStatusGeminiPending, -} - -// RunNextDrainJob to process deposits if there is one waiting -func (pg *Postgres) RunNextDrainJob(ctx context.Context, worker DrainWorker) (bool, error) { - - // setup a logger - logger := logging.Logger(ctx, "promotion.RunNextDrainJob") - - tx, err := pg.RawDB().Beginx() - attempted := false - if err != nil { - return attempted, err - } - defer pg.RollbackTx(tx) - - statement := ` -select * -from claim_drain -where not erred and transaction_id is null -and (status is null or status not in ('complete', 'reputation-failed', 'failed', 'prepared', 'gemini-pending', 'submitted')) -for update skip locked -limit 1` - - jobs := []DrainJob{} - err = tx.Select(&jobs, statement) - if err != nil { - return attempted, err - } - - if len(jobs) != 1 { - return attempted, nil - } - - job := jobs[0] - attempted = true - - // set job status to initialized - _, err = tx.Exec(` - update claim_drain set - status = 'initialized' - where id = $1`, job.ID) - if err != nil { - return attempted, err - } - - var credentials []cbr.CredentialRedemption - err = json.Unmarshal([]byte(job.Credentials), &credentials) - if err != nil { - return attempted, err - } - - if job.Status != nil && (*job.Status == "retry-bypass-cbr" || *job.Status == "manual-retry") { - ctx = context.WithValue(ctx, appctx.SkipRedeemCredentialsCTXKey, true) - } - - txn, err := worker.RedeemAndTransferFunds(ctx, credentials, job) - if err != nil || txn == nil { - // log the error from redeem and transfer - logger.Error().Err(err). - Interface("claim_drain_id", job.ID). - Msg("failed to redeem and transfer funds") - // do not need to capture wallet is not reputable - if !errors.Is(err, errWalletNotReputable) && - !errors.Is(err, errWalletDrainLimitExceeded) && - !errors.Is(err, cbr.ErrDupRedeem) { - // do not sentry log not reputable or drain limit exceeded, or duplicate redemption - sentry.CaptureException(err) - } - - // record as error (retriable or not) - status, errCode, _ := errToDrainCode(err) - if _, err := tx.Exec(` - update claim_drain set - erred = true, - errcode=$1, - status=$3 - where id = $2`, errCode, job.ID, status); err == nil { - _ = tx.Commit() - } - return attempted, err - } - - // if the txn cannot be set as complete immediately then get the status code and update the job - if status, ok := txStatusToStatus[txn.Status]; ok { - _, err = tx.Exec(` - update claim_drain set - transaction_id = $1, - status = $2 - where id = $3`, txn.ID, status, job.ID) - if err != nil { - return attempted, err - } - } else { - countClaimDrainStatus.With(prometheus.Labels{"custodian": "uphold", "status": "complete"}).Inc() - _, err = tx.Exec(` - update claim_drain set - transaction_id = $1, - completed = true, - completed_at = now(), - status = 'complete' - where id = $2`, txn.ID, job.ID) - if err != nil { - return attempted, err - } - } - - err = tx.Commit() - if err != nil { - return attempted, err - } - - return attempted, nil -} - -// RunNextDrainRetryJob - toggles failed drain jobs to be reprocessed if eligible -func (pg *Postgres) RunNextDrainRetryJob(ctx context.Context, worker DrainRetryWorker) error { - for { - select { - case <-ctx.Done(): - return ctx.Err() - default: - walletID, err := worker.FetchAdminAttestationWalletID(ctx) - if err != nil { - return fmt.Errorf("drain retry job: failed to retrieve walletID: %w", err) - } - query := ` - UPDATE claim_drain - SET erred = FALSE, status = 'retry-bypass-cbr' - WHERE wallet_id = $1 AND erred = TRUE AND errcode = 'reputation-failed' AND status = 'reputation-failed' - ` - result, err := pg.ExecContext(ctx, query, walletID.String()) - if err != nil { - err = fmt.Errorf("drain retry job: failed to update drain job for walletID %s: %w ", walletID, err) - logging.FromContext(ctx).Error().Err(err).Msg("") - sentry.CaptureException(err) - } else { - rowsAffected, err := result.RowsAffected() - if err != nil { - err = fmt.Errorf("drain retry job: failed to get rows affected for walletID %s: %w ", walletID, err) - logging.FromContext(ctx).Error().Err(err).Msg("") - sentry.CaptureException(err) - } - if rowsAffected > 0 { - logging.FromContext(ctx).Info(). - Msgf("drain retry job: successfully updated drain job for walletID %s", walletID) - } - } - } - } -} - -// MintDrainJob - Job structure for the mint_drain queue -type MintDrainJob struct { - ID uuid.UUID `db:"id"` - WalletID uuid.UUID `db:"wallet_id"` - Total decimal.Decimal `db:"total"` - Done bool `db:"done"` - Status string `db:"status"` - Erred bool `db:"erred"` -} - -// MintDrainPromotion - a list of promotions associated with a mint job -type MintDrainPromotion struct { - MintJobID uuid.UUID `db:"mint_drain_id"` - PromotionID uuid.UUID `db:"promotion_id"` - Done bool `db:"done"` - Total decimal.Decimal `db:"total"` -} - -const ( - // MintDrainJobPending - pending status for the mint_drain job - MintDrainJobPending = "pending" - // MintDrainJobFailed - failed status for the mint_drain job - MintDrainJobFailed = "failed" - // MintDrainJobComplete - complete status for the mint_drain job - MintDrainJobComplete = "complete" -) - -// RunNextMintDrainJob to process mints vg -func (pg *Postgres) RunNextMintDrainJob(ctx context.Context, worker MintWorker) (bool, error) { - - // setup a logger - logger := logging.Logger(ctx, "promotion.RunNextMintDrainJob") - - // get and parse the correct transfer promotion id to create claims on - braveTransferPromotionIDs, ok := ctx.Value(appctx.BraveTransferPromotionIDCTXKey).([]string) - if !ok { - logger.Error().Err(errMissingTransferPromotion). - Msg("MintJob: missing transfer promotion id") - return false, errMissingTransferPromotion - } - - tx, err := pg.RawDB().Beginx() - attempted := false - if err != nil { - return attempted, err - } - defer pg.RollbackTx(tx) - - // get the mint job. only the ones that have finished all the promotion totals - statement := ` -select md.*, - (select sum(mdp.total) from mint_drain_promotion as mdp where mdp.mint_drain_id=md.id) as total, - (select bool_and(mdp.done) from mint_drain_promotion as mdp where mdp.mint_drain_id=md.id) as done -from mint_drain as md -where not md.erred and md.status = 'pending' and -(select bool_and(done) from mint_drain_promotion where mint_drain_id=md.id) -for update of md skip locked -limit 1; -` - - job := MintDrainJob{} - err = tx.Get(&job, statement) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return attempted, nil - } - return attempted, err - } - // are all of the claims associated with all of the promotions drained? - - statement = ` -select - bool_and(c.drained) -from - mint_drain_promotion mdp - join claims c - on (c.promotion_id=mdp.promotion_id) -where - mdp.mint_drain_id = $1 - and c.wallet_id= $2 -` - var drained bool - err = tx.Get(&drained, statement, job.ID, job.WalletID) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return attempted, nil - } - return attempted, err - } - - if !drained { - return attempted, nil - } - - // yes? set status to complete and mint the grants - promoIDs, err := toUUIDs(braveTransferPromotionIDs...) - if err != nil { - // log the error from redeem and transfer - logger.Error().Err(err).Msg("failed to derive promotion ids from configuration") - return attempted, err - } - - attempted = true - - // mint the grant to the wallet's deposit destination - statement = ` -select - w.user_deposit_destination -from - wallets w -where - w.id = $1 -` - var ( - depositDestination string - ) - err = tx.Get(&depositDestination, statement, job.WalletID) - if err != nil { - return attempted, err - } - - if depositDestination == "" { - return attempted, errors.New("wallet is not verified") - } - - depositDestinationUUID, err := uuid.FromString(depositDestination) - if err != nil { - return attempted, errors.New("destination invalid wallet id") - } - - err = worker.MintGrant(ctx, depositDestinationUUID, job.Total, promoIDs...) - if err != nil { - // log the error from redeem and transfer - logger.Error().Err(err).Msg("failed to mint grants") - if _, err := tx.Exec(`update mint_drain set erred = true where id = $1`, job.ID); err != nil { - pg.RollbackTx(tx) - } - _ = tx.Commit() - return attempted, err - } - - if _, err := tx.Exec(`update mint_drain set status = 'complete' where id = $1`, job.ID); err != nil { - pg.RollbackTx(tx) - } - - err = tx.Commit() - if err != nil { - return attempted, err - } - - return attempted, nil -} - -// UpdateOrder updates the orders status. -// Status should either be one of pending, paid, fulfilled, or canceled. -func (pg *Postgres) UpdateOrder(orderID uuid.UUID, status string) error { - result, err := pg.RawDB().Exec(`UPDATE orders set status = $1, updated_at = CURRENT_TIMESTAMP where id = $2`, status, orderID) - - if err != nil { - return err - } - - rowsAffected, err := result.RowsAffected() - if rowsAffected == 0 || err != nil { - return errors.New("no rows updated") - } - - return nil -} - -// CreateTransaction creates a transaction given an orderID, externalTransactionID, currency, and a kind of transaction -func (pg *Postgres) CreateTransaction(orderID uuid.UUID, externalTransactionID string, status string, currency string, kind string, amount decimal.Decimal) (*Transaction, error) { - tx := pg.RawDB().MustBegin() - defer pg.RollbackTx(tx) - - var transaction Transaction - err := tx.Get(&transaction, - ` - INSERT INTO transactions (order_id, external_transaction_id, status, currency, kind, amount) - VALUES ($1, $2, $3, $4, $5, $6) - RETURNING * - `, orderID, externalTransactionID, status, currency, kind, amount) - - if err != nil { - return nil, err - } - - err = tx.Commit() - if err != nil { - return nil, err - } - - return &transaction, nil -} - -// GetSumForTransactions returns the calculated sum -func (pg *Postgres) GetSumForTransactions(orderID uuid.UUID) (decimal.Decimal, error) { - var sum decimal.Decimal - - err := pg.RawDB().Get(&sum, ` - SELECT SUM(amount) as sum - FROM transactions - WHERE order_id = $1 AND status = 'completed' - `, orderID) - - return sum, err -} - -// UpdateDrainJobAsRetriable - updates a drain job as retriable -func (pg *Postgres) UpdateDrainJobAsRetriable(ctx context.Context, walletID uuid.UUID) error { - query := ` - UPDATE claim_drain - SET erred = FALSE, status = 'manual-retry' - WHERE wallet_id = $1 AND erred = TRUE AND status IN ('reputation-failed', 'failed') AND transaction_id IS NULL - ` - result, err := pg.ExecContext(ctx, query, walletID) - if err != nil { - return fmt.Errorf("update drain job: failed to exec update for walletID %s: %w", walletID, err) - } - - affectedRows, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("update drain job: failed to get affected rows for walletID %s: %w", walletID, err) - } - - if affectedRows == 0 { - return fmt.Errorf("update drain job: failed to update row for walletID %s: %w", walletID, - errorutils.ErrNotFound) - } - - return nil -} - -// RunNextGeminiCheckStatus periodically check the status of gemini claim drain transactions -func (pg *Postgres) RunNextGeminiCheckStatus(ctx context.Context, worker GeminiTxnStatusWorker) (bool, error) { - tx, err := pg.RawDB().Beginx() - if err != nil { - return false, fmt.Errorf("gemini check status job: failed to begin transaction: %w", err) - } - defer pg.RollbackTx(tx) - - var drainJob DrainJob - err = tx.Get(&drainJob, ` - select * from claim_drain - where status = $1 and transaction_id is not null - and updated_at < NOW() - interval '10 MINUTES' - order by updated_at asc - for update skip locked limit 1 - `, txnStatusGeminiPending) - if err != nil { - // no drains to process - if errors.Is(err, sql.ErrNoRows) { - return false, nil - } - return false, fmt.Errorf("gemini check status job: sql error %w", err) - } - - settlementTx := custodian.Transaction{ - SettlementID: ptr.String(drainJob.TransactionID), - Type: "drain", - Destination: ptr.String(drainJob.DepositDestination), - Channel: "wallet", - } - txRef := gemini.GenerateTxRef(&settlementTx) - - transactionInfo, err := worker.GetGeminiTxnStatus(ctx, txRef) - if err != nil || transactionInfo == nil { - - // update the erred claim drain so it goes to back of queue - query := `update claim_drain set status = $1, updated_at = now() where id = $2` - if _, err := tx.ExecContext(ctx, query, txnStatusGeminiPending, drainJob.ID); err != nil { - return true, fmt.Errorf("failed to update status for txn %s: %w", *drainJob.TransactionID, err) - } - - if err := tx.Commit(); err != nil { - return true, fmt.Errorf("failed to commit update status for txn %s: %w", *drainJob.TransactionID, err) - } - - return true, fmt.Errorf("failed to get status for txn %s: %w", *drainJob.TransactionID, err) - } - - switch transactionInfo.Status { - case "complete": - query := `update claim_drain set completed = true, completed_at = now(), status = 'complete' where id = $1` - if _, err := tx.ExecContext(ctx, query, drainJob.ID); err != nil { - return true, fmt.Errorf("failed to update status for txn %s: %w", *drainJob.TransactionID, err) - } - case "pending": - query := `update claim_drain set status = $1, updated_at = now() where id = $2` - if _, err := tx.ExecContext(ctx, query, txnStatusGeminiPending, drainJob.ID); err != nil { - return true, fmt.Errorf("failed to update status for txn %s: %w", *drainJob.TransactionID, err) - } - case "failed": - query := `update claim_drain set status = 'failed', erred = true, errcode = $1 where id = $2` - if _, err := tx.ExecContext(ctx, query, transactionInfo.Note, drainJob.ID); err != nil { - return true, fmt.Errorf("failed to update status for txn %s: %w", *drainJob.TransactionID, err) - } - default: - return true, fmt.Errorf("failed to update status for txn %s: unknown status %s", - *drainJob.TransactionID, transactionInfo.Status) - } - - err = tx.Commit() - if err != nil { - return true, fmt.Errorf("failed to commit update status for txn %s: %w", *drainJob.TransactionID, err) - } - - if transactionInfo.Status == "complete" || transactionInfo.Status == "failed" { - countClaimDrainStatus.With(prometheus.Labels{"custodian": "gemini", "status": transactionInfo.Status}).Inc() - } - - return true, nil -} - -func toUUIDs(a ...string) ([]uuid.UUID, error) { - var ( - b = []uuid.UUID{} - ) - for _, id := range a { - v, err := uuid.FromString(id) - if err != nil { - return nil, err - } - b = append(b, v) - } - return b, nil -} - -// GetDrainsByBatchID - get the drain by the batch id -func (pg *Postgres) GetDrainsByBatchID(ctx context.Context, batchID *uuid.UUID) ([]DrainTransfer, error) { - resp := []DrainTransfer{} - // get the linked wallet info - stmt := ` -select - transaction_id, total, deposit_destination -from - claim_drain -where - batch_id = $1 -` - if err := pg.RawDB().Select(&resp, stmt, batchID); err != nil { - return nil, err - } - - return resp, nil -} diff --git a/services/promotion/datastore_test.go b/services/promotion/datastore_test.go index 7e56c46b1..2ad4f17e8 100644 --- a/services/promotion/datastore_test.go +++ b/services/promotion/datastore_test.go @@ -4,29 +4,9 @@ package promotion import ( "context" - "encoding/json" "errors" - "fmt" - "math/rand" - "sort" - "testing" - "time" - "github.com/brave-intl/bat-go/libs/clients/gemini" - "github.com/brave-intl/bat-go/libs/custodian" - - "github.com/jmoiron/sqlx" - - "github.com/brave-intl/bat-go/libs/ptr" - - "github.com/brave-intl/bat-go/libs/logging" - - appctx "github.com/brave-intl/bat-go/libs/context" - errorutils "github.com/brave-intl/bat-go/libs/errors" - - "github.com/brave-intl/bat-go/libs/clients/cbr" "github.com/brave-intl/bat-go/libs/jsonutils" - testutils "github.com/brave-intl/bat-go/libs/test" walletutils "github.com/brave-intl/bat-go/libs/wallet" "github.com/brave-intl/bat-go/services/wallet" "github.com/golang/mock/gomock" @@ -774,1000 +754,3 @@ func (suite *PostgresTestSuite) TestInsertClobberedClaims() { suite.Require().NoError(err, "selecting the clobbered creds ids should not result in an error") suite.Assert().Equal(allCreds1, allCreds2, "creds should not be inserted more than once") } - -func (suite *PostgresTestSuite) TestDrainClaimErred() { - pg, _, err := NewPostgres() - suite.Require().NoError(err) - - walletDB, _, err := wallet.NewPostgres() - suite.Require().NoError(err) - - publicKey := "hBrtClwIppLmu/qZ8EhGM1TQZUwDUosbOrVu3jMwryY=" - blindedCreds := jsonutils.JSONStringArray([]string{"hBrtClwIppLmu/qZ8EhGM1TQZUwDUosbOrVu3jMwryY="}) - walletID := uuid.NewV4() - wallet2ID := uuid.NewV4() - info := &walletutils.Info{ - ID: walletID.String(), - Provider: "uphold", - ProviderID: uuid.NewV4().String(), - PublicKey: publicKey, - } - info2 := &walletutils.Info{ - ID: wallet2ID.String(), - Provider: "uphold", - ProviderID: uuid.NewV4().String(), - PublicKey: publicKey, - } - err = walletDB.UpsertWallet(context.Background(), info) - err = walletDB.UpsertWallet(context.Background(), info2) - suite.Require().NoError(err, "Upsert wallet must succeed") - - { - tmp := uuid.NewV4() - info.AnonymousAddress = &tmp - } - err = walletDB.UpsertWallet(context.Background(), info) - suite.Require().NoError(err, "Upsert wallet should succeed") - - wallet, err := walletDB.GetWallet(context.Background(), walletID) - suite.Require().NoError(err, "Get wallet should succeed") - suite.Assert().Equal(wallet.AnonymousAddress, info.AnonymousAddress) - - wallet2, err := walletDB.GetWallet(context.Background(), wallet2ID) - suite.Require().NoError(err, "Get wallet should succeed") - - total := decimal.NewFromFloat(50.0) - // Create promotion - promotion, err := pg.CreatePromotion( - "ugp", - 2, - total, - "", - ) - suite.Require().NoError(err, "Create promotion should succeed") - suite.Require().NoError(pg.ActivatePromotion(promotion), "Activate promotion should succeed") - - issuer := &Issuer{PromotionID: promotion.ID, Cohort: "control", PublicKey: publicKey} - issuer, err = pg.InsertIssuer(issuer) - suite.Require().NoError(err, "Insert issuer should succeed") - - claim, err := pg.ClaimForWallet(promotion, issuer, info, blindedCreds) - suite.Require().NoError(err, "Claim creation should succeed") - - suite.Assert().Equal(false, claim.Drained) - - credentials := []cbr.CredentialRedemption{} - - drainID := uuid.NewV4() - - err = pg.DrainClaim(&drainID, claim, credentials, wallet2, total, errMismatchedWallet) - suite.Require().NoError(err, "Drain claim errored call should succeed") - - // should show as drained - claim, err = pg.GetClaimByWalletAndPromotion(wallet, promotion) - suite.Assert().Equal(true, claim.Drained) - - mockCtrl := gomock.NewController(suite.T()) - defer mockCtrl.Finish() - - mockDrainWorker := NewMockDrainWorker(mockCtrl) - - // After err no further job should run - attempted, err := pg.RunNextDrainJob(context.Background(), mockDrainWorker) - suite.Assert().Equal(false, attempted) - suite.Require().NoError(err) - -} - -func (suite *PostgresTestSuite) TestDrainClaim() { - pg, _, err := NewPostgres() - suite.Require().NoError(err) - - walletDB, _, err := wallet.NewPostgres() - suite.Require().NoError(err) - - publicKey := "hBrtClwIppLmu/qZ8EhGM1TQZUwDUosbOrVu3jMwryY=" - blindedCreds := jsonutils.JSONStringArray([]string{"hBrtClwIppLmu/qZ8EhGM1TQZUwDUosbOrVu3jMwryY="}) - walletID := uuid.NewV4() - info := &walletutils.Info{ - ID: walletID.String(), - Provider: "uphold", - ProviderID: uuid.NewV4().String(), - PublicKey: publicKey, - } - err = walletDB.UpsertWallet(context.Background(), info) - suite.Require().NoError(err, "Upsert wallet must succeed") - - { - tmp := uuid.NewV4() - info.AnonymousAddress = &tmp - } - err = walletDB.UpsertWallet(context.Background(), info) - suite.Require().NoError(err, "Upsert wallet should succeed") - - wallet, err := walletDB.GetWallet(context.Background(), walletID) - suite.Require().NoError(err, "Get wallet should succeed") - suite.Assert().Equal(wallet.AnonymousAddress, info.AnonymousAddress) - - total := decimal.NewFromFloat(50.0) - // Create promotion - promotion, err := pg.CreatePromotion( - "ugp", - 2, - total, - "", - ) - suite.Require().NoError(err, "Create promotion should succeed") - suite.Require().NoError(pg.ActivatePromotion(promotion), "Activate promotion should succeed") - - issuer := &Issuer{PromotionID: promotion.ID, Cohort: "control", PublicKey: publicKey} - issuer, err = pg.InsertIssuer(issuer) - suite.Require().NoError(err, "Insert issuer should succeed") - - claim, err := pg.ClaimForWallet(promotion, issuer, info, blindedCreds) - suite.Require().NoError(err, "Claim creation should succeed") - - suite.Assert().Equal(false, claim.Drained) - - credentials := []cbr.CredentialRedemption{} - - drainID := uuid.NewV4() - - err = pg.DrainClaim(&drainID, claim, credentials, wallet, total, nil) - suite.Require().NoError(err, "Drain claim should succeed") - - claim, err = pg.GetClaimByWalletAndPromotion(wallet, promotion) - suite.Assert().Equal(true, claim.Drained) - - mockCtrl := gomock.NewController(suite.T()) - defer mockCtrl.Finish() - - mockDrainWorker := NewMockDrainWorker(mockCtrl) - - // One drain job should run - mockDrainWorker.EXPECT().RedeemAndTransferFunds(gomock.Any(), gomock.Eq(credentials), gomock.Any()).Return(nil, errors.New("Worker failed")) - attempted, err := pg.RunNextDrainJob(context.Background(), mockDrainWorker) - suite.Assert().Equal(true, attempted) - suite.Require().Error(err) - - // After err no further job should run - attempted, err = pg.RunNextDrainJob(context.Background(), mockDrainWorker) - suite.Assert().Equal(false, attempted) - suite.Require().NoError(err) - - // FIXME add test for successful drain job -} - -func (suite *PostgresTestSuite) TestDrainClaims_Success() { - pg, _, err := NewPostgres() - suite.Require().NoError(err) - - walletDB, _, err := wallet.NewPostgres() - suite.Require().NoError(err) - - walletInfo := walletutils.Info{ - ID: uuid.NewV4().String(), - Provider: "uphold", - } - - err = walletDB.UpsertWallet(context.Background(), &walletInfo) - suite.NoError(err) - - drainClaims := make([]DrainClaim, 5) - for i := 0; i < 5; i++ { - total := decimal.NewFromFloat(rand.Float64()) - - promotion, err := pg.CreatePromotion("ugp", 1, - decimal.NewFromFloat(1), testutils.RandomString()) - suite.Require().NoError(err) - - claim, err := pg.CreateClaim(promotion.ID, walletInfo.ID, total, decimal.NewFromFloat(0), false) - suite.Require().NoError(err) - - credentialRedemptions := []cbr.CredentialRedemption{ - { - Issuer: testutils.RandomString(), - TokenPreimage: testutils.RandomString(), - Signature: testutils.RandomString(), - }, - } - - drainClaims[i] = DrainClaim{ - BatchID: ptr.FromUUID(uuid.NewV4()), - Claim: claim, - Credentials: credentialRedemptions, - Wallet: &walletInfo, - Total: total, - CodedErr: nil, - } - } - - err = pg.DrainClaims(drainClaims) - - // assert correct number of claims and claims drains inserted - - var claims []Claim - err = pg.RawDB().Select(&claims, "SELECT * FROM claims") - suite.Require().NoError(err) - suite.Require().Equal(len(drainClaims), len(claims)) - - var claimDrains []DrainJob - err = pg.RawDB().Select(&claimDrains, "SELECT * FROM claim_drain") - suite.Require().NoError(err) - suite.Require().Equal(len(drainClaims), len(claimDrains)) - - // assert the retrieved claims and claims drains inserted are the ones added - - sort.Slice(drainClaims, func(i, j int) bool { - return drainClaims[i].Claim.ID.String() < drainClaims[j].Claim.ID.String() - }) - - sort.Slice(claims, func(i, j int) bool { - return claims[i].ID.String() < claims[j].ID.String() - }) - - sort.Slice(claimDrains, func(i, j int) bool { - return claimDrains[i].ClaimID.String() < claimDrains[j].ClaimID.String() - }) - - for i := 0; i < 5; i++ { - suite.Require().Equal(drainClaims[i].Claim.ID.String(), claims[i].ID.String()) - suite.Require().Equal(drainClaims[i].Claim.ID.String(), claimDrains[i].ClaimID.String()) - } -} - -func (suite *PostgresTestSuite) TestRunNextDrainJob_Gemini_Claim() { - ctrl := gomock.NewController(suite.T()) - defer ctrl.Finish() - - pg, _, err := NewPostgres() - suite.Require().NoError(err) - - drainJob := suite.insertClaimDrainWithStatus(pg, "", false) - - transactionInfo := walletutils.TransactionInfo{} - transactionInfo.Status = txnStatusGeminiPending - - drainWorker := NewMockDrainWorker(ctrl) - drainWorker.EXPECT(). - RedeemAndTransferFunds(gomock.Any(), gomock.Any(), gomock.Any()). - Return(&transactionInfo, nil) - - attempted, err := pg.RunNextDrainJob(context.Background(), drainWorker) - suite.True(attempted) - - // get the updated drain job and assert - err = pg.RawDB().Get(&drainJob, "select * from claim_drain where id = $1", drainJob.ID) - suite.Require().NoError(err) - - suite.Require().Equal(txnStatusGeminiPending, *drainJob.Status) - suite.Require().Equal(false, drainJob.Erred) - suite.Require().Nil(drainJob.ErrCode) -} - -func (suite *PostgresTestSuite) TestDrainRetryJob_Success() { - pg, _, err := NewPostgres() - suite.Require().NoError(err) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - walletID := uuid.NewV4() - - query := `INSERT INTO claim_drain (wallet_id, erred, errcode, status, batch_id, credentials, completed, total) - VALUES ($1, $2, $3, $4, $5, '[{"t":"123"}]', FALSE, 1);` - - _, err = pg.RawDB().ExecContext(context.Background(), query, walletID.String(), true, "reputation-failed", "reputation-failed", - uuid.NewV4().String()) - suite.Require().NoError(err, "should have inserted claim drain row") - - ctrl := gomock.NewController(suite.T()) - defer ctrl.Finish() - - drainRetryWorker := NewMockDrainRetryWorker(ctrl) - drainRetryWorker.EXPECT(). - FetchAdminAttestationWalletID(gomock.Eq(ctx)). - Return(&walletID, nil). - AnyTimes() - - go func(ctx2 context.Context) { - pg.RunNextDrainRetryJob(ctx2, drainRetryWorker) - }(ctx) - - time.Sleep(1 * time.Millisecond) - - var drainJob DrainJob - err = pg.RawDB().Get(&drainJob, `SELECT * FROM claim_drain WHERE wallet_id = $1 LIMIT 1`, walletID) - suite.Require().NoError(err, "should have retrieved drain job") - - suite.Require().Equal(walletID, drainJob.WalletID) - suite.Require().Equal(false, drainJob.Erred) - suite.Require().Equal("reputation-failed", *drainJob.ErrCode) - suite.Require().Equal("retry-bypass-cbr", *drainJob.Status) -} - -func (suite *PostgresTestSuite) TestRunNextBatchPaymentsJob_NoClaimsToProcess() { - pg, _, err := NewPostgres() - suite.Require().NoError(err) - - ctrl := gomock.NewController(suite.T()) - defer ctrl.Finish() - - batchTransferWorker := NewMockBatchTransferWorker(ctrl) - - actual, err := pg.RunNextBatchPaymentsJob(context.Background(), batchTransferWorker) - suite.Require().NoError(err) - suite.Require().False(actual, "should not have attempted job run") -} - -func (suite *PostgresTestSuite) TestRunNextBatchPaymentsJob_SubmitBatchTransfer_Error() { - ctrl := gomock.NewController(suite.T()) - defer ctrl.Finish() - - pg, _, err := NewPostgres() - suite.Require().NoError(err) - - walletDB, _, err := wallet.NewPostgres() - suite.Require().NoError(err) - - ctx, _ := logging.SetupLogger(context.Background()) - - // setup wallet - walletID := uuid.NewV4() - userDepositAccountProvider := "bitflyer" - - info := &walletutils.Info{ - ID: walletID.String(), - Provider: "uphold", - ProviderID: uuid.NewV4().String(), - PublicKey: "hBrtClwIppLmu/qZ8EhGM1TQZUwDUosbOrVu3jMwryY=", - UserDepositAccountProvider: &userDepositAccountProvider, - } - err = walletDB.UpsertWallet(ctx, info) - suite.Require().NoError(err) - - // setup claim drain - batchID := uuid.NewV4() - - query := `INSERT INTO claim_drain (wallet_id, erred, errcode, status, batch_id, credentials, completed, total, transaction_id) - VALUES ($1, $2, $3, $4, $5, '[{"t":123}]', FALSE, 1, $6);` - - _, err = pg.RawDB().ExecContext(context.Background(), query, walletID, false, nil, "prepared", batchID, uuid.NewV4().String()) - suite.Require().NoError(err, "should have inserted claim drain row") - - drainCodeErr := drainCodeErrorInvalidDepositID - drainCodeError := errorutils.New(errors.New("error-text"), - "error-message", drainCodeErr) - - batchTransferWorker := NewMockBatchTransferWorker(ctrl) - batchTransferWorker.EXPECT(). - SubmitBatchTransfer(ctx, &batchID). - Return(drainCodeError) - - actual, actualErr := pg.RunNextBatchPaymentsJob(ctx, batchTransferWorker) - - var drainJob DrainJob - err = pg.RawDB().Get(&drainJob, `SELECT * FROM claim_drain WHERE wallet_id = $1 LIMIT 1`, walletID) - suite.Require().NoError(err, "should have retrieved drain job") - - suite.Require().Equal(walletID, drainJob.WalletID) - suite.Require().Equal(true, drainJob.Erred) - suite.Require().Equal(drainCodeErr.ErrCode, *drainJob.ErrCode) - suite.Require().Equal("failed", *drainJob.Status) - - suite.Require().True(actual, "should have attempted job run") - suite.Require().Equal(drainCodeError, actualErr) -} - -// tests batches are only processed once the drain job has set all claims drains -// to prepared and have they have transactionIDs -func (suite *PostgresTestSuite) TestRunNextBatchPaymentsJob_NextDrainJob_Concurrent() { - - suite.CleanDB() - - ctrl := gomock.NewController(suite.T()) - defer ctrl.Finish() - - pg, _, err := NewPostgres() - suite.Require().NoError(err) - - walletDB, _, err := wallet.NewPostgres() - suite.Require().NoError(err) - - ctx, cancel := context.WithCancel(context.Background()) - - walletID := uuid.NewV4() - batchID := uuid.NewV4() - - info := &walletutils.Info{ - ID: walletID.String(), - Provider: "uphold", - ProviderID: uuid.NewV4().String(), - PublicKey: "hBrtClwIppLmu/qZ8EhGM1TQZUwDUosbOrVu3jMwryY=", - UserDepositAccountProvider: ptr.FromString("bitflyer"), - } - err = walletDB.UpsertWallet(ctx, info) - suite.Require().NoError(err) - - // setup 3 claim drains and 1 erred claim drain in batch - for i := 0; i < 3; i++ { - claimDrainFixtures(pg.RawDB(), batchID, walletID, false, false) - } - // setup one erred job as part of batch which should not get run by batch payment - claimDrainFixtures(pg.RawDB(), batchID, walletID, false, true) - - transactionInfo := walletutils.TransactionInfo{} - transactionInfo.Status = "bitflyer-consolidate" - transactionInfo.ID = uuid.NewV4().String() - - drainWorker := NewMockDrainWorker(ctrl) - drainWorker.EXPECT(). - RedeemAndTransferFunds(gomock.Any(), gomock.Any(), gomock.Any()). - Return(&transactionInfo, nil). - Times(3) - - batchTransferWorker := NewMockBatchTransferWorker(ctrl) - batchTransferWorker.EXPECT(). - SubmitBatchTransfer(gomock.Any(), gomock.Any()). - Return(nil). - Times(1) - - // start batch payments job to pick up claim drains when all in batch are in prepared state - go func(ctx context.Context) { - for { - select { - case <-ctx.Done(): - return - default: - pg.RunNextBatchPaymentsJob(context.Background(), batchTransferWorker) - } - } - }(ctx) - - // run next drain job to pickup job and set as prepared and set transactionID - - attempted, err := pg.RunNextDrainJob(ctx, drainWorker) - suite.Require().True(attempted) - suite.NoError(err) - - time.Sleep(200 * time.Millisecond) - - var drainJobsFirst []DrainJob - err = pg.RawDB().Select(&drainJobsFirst, `SELECT * FROM claim_drain`) - suite.Require().NoError(err, "should have retrieved drain job") - - // check no jobs have been submitted to batch and one job are prepared - prepared := 0 - for _, drain := range drainJobsFirst { - suite.Require().NotEqual("submitted", ptr.String(drain.Status), - fmt.Sprintf("should not be submitted got %s", ptr.String(drain.Status))) - if ptr.String(drain.Status) == "prepared" { - prepared += 1 - } - } - suite.Require().Equal(1, prepared) - - // run next drain job to pickup job and set as prepared and set transactionID - - attempted, err = pg.RunNextDrainJob(ctx, drainWorker) - suite.Require().True(attempted) - suite.NoError(err) - - time.Sleep(200 * time.Millisecond) - - var drainJobsSecond []DrainJob - err = pg.RawDB().Select(&drainJobsSecond, `SELECT * FROM claim_drain`) - suite.Require().NoError(err, "should have retrieved drain job") - - // check no jobs have been submitted to batch and two job are prepared - prepared = 0 - for _, drain := range drainJobsSecond { - suite.Require().NotEqual("submitted", ptr.String(drain.Status), - fmt.Sprintf("should not be submitted got %s", ptr.String(drain.Status))) - if ptr.String(drain.Status) == "prepared" { - prepared += 1 - } - } - suite.Require().Equal(2, prepared) - - // run final next drain job to set claim drain as prepared and transactionID - - attempted, err = pg.RunNextDrainJob(ctx, drainWorker) - suite.Require().True(attempted) - suite.NoError(err) - - time.Sleep(200 * time.Millisecond) - - // run batch payments should now pickup all claims in batch and process - var actual []DrainJob - err = pg.RawDB().Select(&actual, `SELECT * FROM claim_drain`) - suite.Require().NoError(err, "should have retrieved drain job") - - suite.Require().Equal(4, len(actual)) - - // assert we have 3 submitted and 1 erred claim drain - submitted := 0 - erred := 0 - for _, drain := range actual { - // submitted - if !drain.Erred { - submitted += 1 - suite.Require().Equal("submitted", ptr.String(drain.Status), - fmt.Sprintf("should be submitted got %s", ptr.String(drain.Status))) - suite.Require().Equal(batchID, *drain.BatchID) - suite.Require().NotNil(drain.TransactionID) - } - // erred - if drain.Erred { - erred += 1 - suite.Require().Equal(batchID, *drain.BatchID) - suite.Require().Nil(drain.TransactionID) - } - } - suite.Require().Equal(3, submitted) - suite.Require().Equal(1, erred) - - // shutdown bath payments job routine - cancel() -} - -func (suite *PostgresTestSuite) TestUpdateDrainJobAsRetriable_Success() { - pg, _, err := NewPostgres() - suite.Require().NoError(err) - - walletID := uuid.NewV4() - - query := `INSERT INTO claim_drain (wallet_id, erred, errcode, status, batch_id, credentials, completed, total) - VALUES ($1, $2, $3, $4, $5, '[{"t":"123"}]', FALSE, 1);` - - _, err = pg.RawDB().ExecContext(context.Background(), query, walletID, true, "some-failed-errcode", "failed", - uuid.NewV4().String()) - suite.Require().NoError(err, "should have inserted claim drain row") - - err = pg.UpdateDrainJobAsRetriable(context.Background(), walletID) - suite.Require().NoError(err, "should have updated claim drain row") - - var drainJob DrainJob - err = pg.RawDB().Get(&drainJob, `SELECT * FROM claim_drain WHERE wallet_id = $1 LIMIT 1`, walletID) - suite.Require().NoError(err, "should have retrieved drain job") - - suite.Require().Equal(walletID, drainJob.WalletID) - suite.Require().Equal(false, drainJob.Erred) - suite.Require().Equal("manual-retry", *drainJob.Status) -} - -func (suite *PostgresTestSuite) TestUpdateDrainJobAsRetriable_NotFound_WalletID() { - pg, _, err := NewPostgres() - suite.Require().NoError(err) - - query := `INSERT INTO claim_drain (wallet_id, erred, errcode, status, batch_id, credentials, completed, total) - VALUES ($1, $2, $3, $4, $5, '[{"t":"123"}]', FALSE, 1);` - - _, err = pg.RawDB().ExecContext(context.Background(), query, uuid.NewV4(), true, "some-failed-errcode", "failed", - uuid.NewV4().String()) - suite.Require().NoError(err, "should have inserted claim drain row") - - walletID := uuid.NewV4() - err = pg.UpdateDrainJobAsRetriable(context.Background(), walletID) - - expected := fmt.Errorf("update drain job: failed to update row for walletID %s: %w", walletID, - errorutils.ErrNotFound) - - suite.Require().Error(err, expected.Error()) -} - -func (suite *PostgresTestSuite) TestUpdateDrainJobAsRetriable_NoRetriableJobFound() { - pg, _, err := NewPostgres() - suite.Require().NoError(err) - - query := `INSERT INTO claim_drain (wallet_id, erred, errcode, status, batch_id, credentials, completed, total) - VALUES ($1, $2, $3, $4, $5, '[{"t":"123"}]', FALSE, 1);` - - walletID := uuid.NewV4() - - _, err = pg.RawDB().ExecContext(context.Background(), query, walletID, true, "some-errcode", "complete", - uuid.NewV4()) - suite.Require().NoError(err, "should have inserted claim drain row") - - err = pg.UpdateDrainJobAsRetriable(context.Background(), walletID) - - expected := fmt.Errorf("update drain job: failed to update row for walletID %s: %w", walletID, - errorutils.ErrNotFound) - - suite.Require().Error(err, expected.Error()) -} - -func (suite *PostgresTestSuite) TestUpdateDrainJobAsRetriable_NotFound_Erred() { - pg, _, err := NewPostgres() - suite.Require().NoError(err) - - query := `INSERT INTO claim_drain (wallet_id, erred, errcode, status, batch_id, credentials, completed, total) - VALUES ($1, $2, $3, $4, $5, '[{"t":"123"}]', FALSE, 1);` - - walletID := uuid.NewV4() - erred := false - - _, err = pg.RawDB().ExecContext(context.Background(), query, walletID, erred, "some-failed-errcode", "failed", - uuid.NewV4()) - suite.Require().NoError(err, "should have inserted claim drain row") - - err = pg.UpdateDrainJobAsRetriable(context.Background(), walletID) - - expected := fmt.Errorf("update drain job: failed to update row for walletID %s: %w", walletID, - errorutils.ErrNotFound) - - suite.Require().Error(err, expected.Error()) -} - -func (suite *PostgresTestSuite) TestUpdateDrainJobAsRetriable_NotFound_TransactionID() { - pg, _, err := NewPostgres() - suite.Require().NoError(err) - - query := `INSERT INTO claim_drain (wallet_id, erred, errcode, status, batch_id, credentials, completed, total, transaction_id) - VALUES ($1, $2, $3, $4, $5, '[{"t":"123"}]', FALSE, 1, $6);` - - walletID := uuid.NewV4() - - _, err = pg.RawDB().ExecContext(context.Background(), query, walletID, true, "some-failed-errcode", "failed", - uuid.NewV4(), uuid.NewV4()) - suite.Require().NoError(err, "should have inserted claim drain row") - - err = pg.UpdateDrainJobAsRetriable(context.Background(), walletID) - - expected := fmt.Errorf("update drain job: failed to update row for walletID %s: %w", walletID, - errorutils.ErrNotFound) - - suite.Require().Error(err, expected.Error()) -} - -func (suite *PostgresTestSuite) TestRunNextDrainJob_CBRBypass_ManualRetry() { - // clean db so only one claim drain job selectable - suite.CleanDB() - - pg, _, err := NewPostgres() - suite.Require().NoError(err) - - walletID := uuid.NewV4() - - credentialRedemption := cbr.CredentialRedemption{ - Issuer: testutils.RandomString(), - TokenPreimage: testutils.RandomString(), - Signature: testutils.RandomString(), - } - credentialRedemptions := make([]cbr.CredentialRedemption, 0) - credentialRedemptions = append(credentialRedemptions, credentialRedemption) - - credentials, err := json.Marshal(credentialRedemptions) - suite.Require().NoError(err, "should have serialised credentials") - - query := `INSERT INTO claim_drain (wallet_id, erred, errcode, status, batch_id, credentials, completed, total) - VALUES ($1, FALSE, 'some-errcode', 'manual-retry', $2, $3, FALSE, 1);` - - _, err = pg.RawDB().ExecContext(context.Background(), query, walletID, uuid.NewV4().String(), credentials) - suite.Require().NoError(err, "should have inserted claim drain row") - - // expected context with bypass cbr true - ctrl := gomock.NewController(suite.T()) - drainWorker := NewMockDrainWorker(ctrl) - - ctx := context.Background() - - drainWorker.EXPECT(). - RedeemAndTransferFunds(isCBRBypass(ctx), credentialRedemptions, gomock.Any()). - Return(&walletutils.TransactionInfo{}, nil) - - attempted, err := pg.RunNextDrainJob(ctx, drainWorker) - - suite.Require().NoError(err, "should have been successful attempted job") - suite.Require().True(attempted) -} - -func (suite *PostgresTestSuite) TestRunNextGeminiCheckStatus_Complete() { - // clean db so only one claim drain job selectable - suite.CleanDB() - - pg, _, err := NewPostgres() - suite.Require().NoError(err) - - ctx := context.Background() - - drainJob := suite.insertClaimDrainWithStatus(pg, txnStatusGeminiPending, true) - - // create tx_ref - settlementTx := custodian.Transaction{ - SettlementID: ptr.String(drainJob.TransactionID), - Type: "drain", - Destination: ptr.String(drainJob.DepositDestination), - Channel: "wallet", - } - txRef := gemini.GenerateTxRef(&settlementTx) - - txnStatus := &walletutils.TransactionInfo{Status: "complete"} - - ctrl := gomock.NewController(suite.T()) - geminiTxnStatusWorker := NewMockGeminiTxnStatusWorker(ctrl) - geminiTxnStatusWorker.EXPECT(). - GetGeminiTxnStatus(ctx, txRef). - Return(txnStatus, nil) - - attempted, err := pg.RunNextGeminiCheckStatus(ctx, geminiTxnStatusWorker) - suite.Require().NoError(err, "should be no error") - suite.Require().True(attempted) - - err = pg.RawDB().Get(&drainJob, "select * from claim_drain where id = $1", drainJob.ID) - suite.Require().NoError(err) - - suite.Require().Equal("complete", *drainJob.Status) - suite.Require().True(drainJob.Completed) - suite.Require().False(drainJob.Erred) -} - -func (suite *PostgresTestSuite) TestRunNextGeminiCheckStatus_Pending() { - // clean db so only one claim drain job selectable - suite.CleanDB() - - pg, _, err := NewPostgres() - suite.Require().NoError(err) - - ctrl := gomock.NewController(suite.T()) - geminiTxnStatusWorker := NewMockGeminiTxnStatusWorker(ctrl) - - ctx := context.Background() - txnStatus := &walletutils.TransactionInfo{Status: "pending"} - - // insert drain jobs in pending state and setup mock call to get status - drainJobs := [5]DrainJob{} - for i := 0; i < len(drainJobs); i++ { - drainJob := suite.insertClaimDrainWithStatus(pg, txnStatusGeminiPending, true) - - // create tx_ref - settlementTx := custodian.Transaction{ - SettlementID: ptr.String(drainJob.TransactionID), - Type: "drain", - Destination: ptr.String(drainJob.DepositDestination), - Channel: "wallet", - } - txRef := gemini.GenerateTxRef(&settlementTx) - - geminiTxnStatusWorker.EXPECT(). - GetGeminiTxnStatus(ctx, txRef). - Return(txnStatus, nil). - Times(1) - - drainJobs[i] = drainJob - } - - // check all claim drains are processed in the order they were inserted earliest to latest date - for i := 0; i < len(drainJobs); i++ { - attempted, err := pg.RunNextGeminiCheckStatus(ctx, geminiTxnStatusWorker) - suite.Require().NoError(err, "should be no error") - suite.Require().True(attempted) - - err = pg.RawDB().Get(&drainJobs[i], "select * from claim_drain where id = $1", drainJobs[i].ID) - suite.Require().NoError(err) - - suite.Require().Equal(txnStatusGeminiPending, *drainJobs[i].Status) - suite.Require().False(drainJobs[i].Completed) - suite.Require().False(drainJobs[i].Erred) - } - - // should return no jobs as we wait 10 mins before retrying - attempted, err := pg.RunNextGeminiCheckStatus(ctx, geminiTxnStatusWorker) - suite.Require().NoError(err, "should be no error") - suite.Require().False(attempted) - - // retrieve next job to run this should be the first inserted job in the cycle - var nextDrainJob DrainJob - err = pg.RawDB().Get(&nextDrainJob, "select * from claim_drain order by updated_at asc limit 1") - - suite.Require().NoError(err) - suite.Require().Equal(drainJobs[0].ID, nextDrainJob.ID, "should have been first job we inserted") -} - -func (suite *PostgresTestSuite) TestRunNextGeminiCheckStatus_Failure() { - // clean db so only one claim drain job selectable - suite.CleanDB() - - pg, _, err := NewPostgres() - suite.Require().NoError(err) - - ctx := context.Background() - - drainJob := suite.insertClaimDrainWithStatus(pg, txnStatusGeminiPending, true) - - // create tx_ref - settlementTx := custodian.Transaction{ - SettlementID: ptr.String(drainJob.TransactionID), - Type: "drain", - Destination: ptr.String(drainJob.DepositDestination), - Channel: "wallet", - } - txRef := gemini.GenerateTxRef(&settlementTx) - - note := testutils.RandomString() - txnStatus := &walletutils.TransactionInfo{Status: "failed", Note: note} - - ctrl := gomock.NewController(suite.T()) - geminiTxnStatusWorker := NewMockGeminiTxnStatusWorker(ctrl) - geminiTxnStatusWorker.EXPECT(). - GetGeminiTxnStatus(ctx, txRef). - Return(txnStatus, nil) - - attempted, err := pg.RunNextGeminiCheckStatus(ctx, geminiTxnStatusWorker) - suite.Require().NoError(err, "should be no error") - suite.Require().True(attempted) - - err = pg.RawDB().Get(&drainJob, "select * from claim_drain where id = $1", drainJob.ID) - suite.Require().NoError(err) - - suite.Require().Equal("failed", *drainJob.Status) - suite.Require().Equal(true, drainJob.Erred) - suite.Require().Equal(note, *drainJob.ErrCode) -} - -func (suite *PostgresTestSuite) TestRunNextGeminiCheckStatus_GetGeminiTxnStatus_Error() { - // clean db so only one claim drain job selectable - suite.CleanDB() - - pg, _, err := NewPostgres() - suite.Require().NoError(err) - - ctx := context.Background() - - drainJob := suite.insertClaimDrainWithStatus(pg, txnStatusGeminiPending, true) - - // create tx_ref - settlementTx := custodian.Transaction{ - SettlementID: ptr.String(drainJob.TransactionID), - Type: "drain", - Destination: ptr.String(drainJob.DepositDestination), - Channel: "wallet", - } - txRef := gemini.GenerateTxRef(&settlementTx) - - getGeminiTxnStatusError := fmt.Errorf(testutils.RandomString()) - - ctrl := gomock.NewController(suite.T()) - geminiTxnStatusWorker := NewMockGeminiTxnStatusWorker(ctrl) - geminiTxnStatusWorker.EXPECT(). - GetGeminiTxnStatus(ctx, txRef). - Return(nil, getGeminiTxnStatusError) - - attempted, err := pg.RunNextGeminiCheckStatus(ctx, geminiTxnStatusWorker) - - suite.Require().Errorf(err, fmt.Sprintf("failed to get status for txn %s: %s", - *drainJob.TransactionID, getGeminiTxnStatusError.Error())) - - suite.Require().True(attempted) - - err = pg.RawDB().Get(&drainJob, "select * from claim_drain where id = $1", drainJob.ID) - suite.Require().NoError(err) - - suite.Require().Equal(txnStatusGeminiPending, *drainJob.Status) - suite.Require().Equal(false, drainJob.Erred) - suite.Require().Nil(drainJob.ErrCode) -} - -func (suite *PostgresTestSuite) TestGetDrainPoll() { - pg, _, err := NewPostgres() - suite.Require().NoError(err, "Failed to get postgres conn") - - walletID := uuid.NewV4() - - completeID := uuid.NewV4() - delayedID := uuid.NewV4() - pendingID := uuid.NewV4() - inprogressID := uuid.NewV4() - - err = claimDrainFixtures(pg.RawDB(), completeID, walletID, true, false) - suite.Require().NoError(err, "failed to fixture claim_drain") - - err = claimDrainFixtures(pg.RawDB(), delayedID, walletID, false, true) - suite.Require().NoError(err, "failed to fixture claim_drain") - - err = claimDrainFixtures(pg.RawDB(), pendingID, walletID, false, false) - suite.Require().NoError(err, "failed to fixture claim_drain") - - err = claimDrainFixtures(pg.RawDB(), inprogressID, walletID, true, false) - suite.Require().NoError(err, "failed to fixture claim_drain") - - err = claimDrainFixtures(pg.RawDB(), inprogressID, walletID, false, false) - suite.Require().NoError(err, "failed to fixture claim_drain") - - service := &Service{ - Datastore: pg, - } - - drainPoll, err := service.Datastore.GetDrainPoll(&completeID) - suite.Require().NoError(err, "Failed to get drain poll response") - suite.Require().True(drainPoll.Status == "complete") - - drainPoll, err = service.Datastore.GetDrainPoll(&delayedID) - suite.Require().NoError(err, "Failed to get drain poll response") - suite.Require().True(drainPoll.Status == "delayed") - - drainPoll, err = service.Datastore.GetDrainPoll(&pendingID) - suite.Require().NoError(err, "Failed to get drain poll response") - suite.Require().True(drainPoll.Status == "pending") - - drainPoll, err = service.Datastore.GetDrainPoll(&inprogressID) - suite.Require().NoError(err, "Failed to get drain poll response") - suite.Require().True(drainPoll.Status == "in_progress") - - // unknown batch_id - unknownID := uuid.NewV4() - drainPoll, err = service.Datastore.GetDrainPoll(&unknownID) - suite.Require().NoError(err, "Failed to get drain poll response") - - suite.Require().True(drainPoll.Status == "unknown") -} - -func claimDrainFixtures(db *sqlx.DB, batchID, walletID uuid.UUID, completed, erred bool) error { - _, err := db.Exec(`INSERT INTO claim_drain (batch_id, credentials, completed, erred, wallet_id, total, updated_at) - values ($1, '[{"t":"123"}]', $2, $3, $4, $5, CURRENT_TIMESTAMP);`, batchID, completed, erred, walletID, 1) - return err -} - -func (suite *PostgresTestSuite) insertClaimDrainWithStatus(pg Datastore, status string, hasTransaction bool) DrainJob { - walletID := uuid.NewV4() - - var transactionID *uuid.UUID - if hasTransaction { - transactionID = ptr.FromUUID(uuid.NewV4()) - } - - credentialRedemption := cbr.CredentialRedemption{ - Issuer: testutils.RandomString(), - TokenPreimage: testutils.RandomString(), - Signature: testutils.RandomString(), - } - credentialRedemptions := make([]cbr.CredentialRedemption, 0) - credentialRedemptions = append(credentialRedemptions, credentialRedemption) - - credentials, err := json.Marshal(credentialRedemptions) - suite.Require().NoError(err, "should have serialized credentials") - - query := `INSERT INTO claim_drain (credentials, wallet_id, total, transaction_id, erred, status, completed, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, now() - (interval '11 MINUTE')) RETURNING *;` - - var drainJob DrainJob - err = pg.RawDB().Get(&drainJob, query, credentials, walletID, 1, transactionID, false, status, false) - suite.Require().NoError(err, "should have inserted and returned claim drain row") - - return drainJob -} - -func isCBRBypass(ctx context.Context) gomock.Matcher { - return cbrBypass{ctx: ctx} -} - -type cbrBypass struct { - ctx context.Context -} - -func (c cbrBypass) Matches(arg interface{}) bool { - ctx := arg.(context.Context) - return ctx.Value(appctx.SkipRedeemCredentialsCTXKey) == true -} - -func (c cbrBypass) String() string { - return "failed: cbr bypass is false" -} - -func TestPostgresTestSuite(t *testing.T) { - suite.Run(t, new(PostgresTestSuite)) -} - -func getClaimDrainEntry(pg *Postgres) *DrainJob { - var dj = new(DrainJob) - statement := `select * from claim_drain limit 1` - _ = pg.Get(dj, statement) - return dj -} - -func getSuggestionDrainEntry(pg *Postgres) *SuggestionJob { - var sj = new(SuggestionJob) - statement := `select * from suggestion_drain limit 1` - _ = pg.Get(sj, statement) - return sj -} diff --git a/services/promotion/drain.go b/services/promotion/drain.go index ec11c7f5a..90032407d 100644 --- a/services/promotion/drain.go +++ b/services/promotion/drain.go @@ -5,40 +5,15 @@ import ( "encoding/json" "errors" "fmt" - "net/http" - "os" - "strings" - "time" - "github.com/brave-intl/bat-go/libs/custodian" - "github.com/brave-intl/bat-go/libs/ptr" - - "github.com/brave-intl/bat-go/libs/altcurrency" - "github.com/brave-intl/bat-go/libs/clients" - "github.com/brave-intl/bat-go/libs/clients/bitflyer" - "github.com/brave-intl/bat-go/libs/clients/cbr" - "github.com/brave-intl/bat-go/libs/clients/gemini" - "github.com/brave-intl/bat-go/libs/clients/reputation" - appctx "github.com/brave-intl/bat-go/libs/context" - "github.com/brave-intl/bat-go/libs/cryptography" - errorutils "github.com/brave-intl/bat-go/libs/errors" "github.com/brave-intl/bat-go/libs/logging" - "github.com/brave-intl/bat-go/libs/middleware" - walletutils "github.com/brave-intl/bat-go/libs/wallet" - "github.com/getsentry/sentry-go" "github.com/lib/pq" "github.com/prometheus/client_golang/prometheus" uuid "github.com/satori/go.uuid" "github.com/shopspring/decimal" ) -const ( - txnStatusGeminiPending string = "gemini-pending" -) - var ( - errMissingTransferPromotion = errors.New("missing configuration: BraveTransferPromotionID") - errGeminiMisconfigured = errors.New("gemini is not configured") errReputationServiceFailure = errors.New("failed to call reputation service") errWalletNotReputable = errors.New("wallet is not reputable") errWalletDrainLimitExceeded = errors.New("wallet drain limit exceeded") @@ -50,738 +25,6 @@ var ( }) ) -// Drain ad suggestions into verified wallet -func (service *Service) Drain(ctx context.Context, credentials []CredentialBinding, walletID uuid.UUID) (*uuid.UUID, error) { - - logger := logging.Logger(ctx, "promotion.Drain") - - var batchID = uuid.NewV4() - - sublogger := logger.With(). - Str("wallet_id", walletID.String()). - Str("batch_id", batchID.String()). - Logger() - - wallet, err := service.wallet.Datastore.GetWallet(ctx, walletID) - if err != nil || wallet == nil { - sublogger.Error().Err(err).Msg("failed to get wallet by id") - return nil, fmt.Errorf("error getting wallet: %w", err) - } - - // A verified wallet will have a payout address - if wallet.UserDepositDestination == "" { - sublogger.Error().Err(err).Msg("wallet is not linked/verified") - return nil, errors.New("wallet is not verified") - } - - // Iterate through each credential and assemble list of funding sources - _, _, fundingSources, promotions, err := service.GetCredentialRedemptions(ctx, credentials) - if err != nil { - sublogger.Error().Err(err).Msg("failed to get credential redemptions") - return nil, err - } - var ( - depositProvider string - ) - if wallet.UserDepositAccountProvider != nil { - depositProvider = *wallet.UserDepositAccountProvider - } - - // if this is a brave wallet with a user deposit destination, we need to create a - // mint drain job in waiting status, waiting for all promotions to be added to it - if depositProvider == "brave" && wallet.UserDepositDestination != "" { - // first let's make sure this wallet is an ios attested device... - - ctx = context.WithValue(ctx, appctx.WalletOnPlatformPriorToCTXKey, os.Getenv("WALLET_ON_PLATFORM_PRIOR_TO")) - // is this from wallet reputable as an iOS device? - isFromOnPlatform, err := service.reputationClient.IsWalletOnPlatform(ctx, walletID, "ios") - if err != nil { - sublogger.Error().Err(err).Str("provider", "brave").Msg("wallet is not on ios platform") - return nil, fmt.Errorf("invalid device: %w", err) - } - - if !isFromOnPlatform { - // wallet is not reputable, decline - sublogger.Error().Str("provider", "brave").Msg("wallet is not on ios platform") - return nil, fmt.Errorf("unable to drain to wallet: invalid device") - } - - // these drained claims commit to mint - var promotionIDs = []uuid.UUID{} - for k := range fundingSources { - promotionIDs = append(promotionIDs, promotions[k].ID) - } - - walletID, err := uuid.FromString(wallet.ID) - if err != nil { - sublogger.Error().Str("provider", "brave").Msg("wallet id is invalid") - return nil, fmt.Errorf("invalid wallet id: %w", err) - } - - err = service.Datastore.EnqueueMintDrainJob(ctx, walletID, promotionIDs...) - if err != nil { - sublogger.Error().Str("provider", "brave").Msg("failed to add ios transfer job") - return nil, fmt.Errorf("error adding mint drain: %w", err) - } - } - - drainClaims := make([]DrainClaim, 0) - for k, v := range fundingSources { - var promotion = promotions[k] - - // if the type is not ads - // except in the case the promotion is for ios and deposit provider is a brave wallet - if v.Type != "ads" && depositProvider != "brave" && strings.ToLower(promotion.Platform) != "ios" { - sublogger.Error().Msg("invalid promotion platform, must be ads") - continue - } - - claim, err := service.Datastore.GetClaimByWalletAndPromotion(wallet, promotion) - if err != nil || claim == nil { - sublogger.Error().Err(err).Str("promotion_id", promotion.ID.String()).Msg("claim does not exist for wallet") - // the case where there this wallet never got this promotion - drainClaims = append(drainClaims, DrainClaim{ - BatchID: &batchID, - Claim: claim, - Credentials: v.Credentials, - Wallet: wallet, - Total: v.Amount, - CodedErr: errMismatchedWallet, - }) - continue - } - - suggestionsExpected, err := claim.SuggestionsNeeded(promotion) - if err != nil { - sublogger.Error().Err(err).Str("promotion_id", promotion.ID.String()).Msg("invalid number of suggestions") - // the case where there is an invalid number of suggestions - drainClaims = append(drainClaims, DrainClaim{ - BatchID: &batchID, - Claim: claim, - Credentials: v.Credentials, - Wallet: wallet, - Total: v.Amount, - CodedErr: errInvalidSuggestionCount, - }) - continue - } - - amountExpected := decimal.New(int64(suggestionsExpected), 0).Mul(promotion.CredentialValue()) - if v.Amount.GreaterThan(amountExpected) { - sublogger.Error().Str("promotion_id", promotion.ID.String()).Msg("attempting to claim more funds than earned") - // the case where there the amount is higher than expected - drainClaims = append(drainClaims, DrainClaim{ - BatchID: &batchID, - Claim: claim, - Credentials: v.Credentials, - Wallet: wallet, - Total: v.Amount, - CodedErr: errInvalidSuggestionAmount, - }) - continue - } - - // skip already drained promotions for idempotency - if !claim.Drained { - drainClaims = append(drainClaims, DrainClaim{ - BatchID: &batchID, - Claim: claim, - Credentials: v.Credentials, - Wallet: wallet, - Total: v.Amount, - CodedErr: nil, - }) - } - } - - if len(drainClaims) > 0 { - err = service.Datastore.DrainClaims(drainClaims) - if err != nil { - return nil, fmt.Errorf("faied to insert drain claims for walletID %s: %w", walletID, err) - } - } - - for i := 0; i < len(drainClaims); i++ { - // the original request context will be cancelled as soon as the dialer closes the connection. - // this will setup a new context with the same values and a 90 second timeout - asyncCtx, asyncCancel := context.WithTimeout(context.Background(), 90*time.Second) - scopedCtx := appctx.Wrap(ctx, asyncCtx) - - go func() { - defer asyncCancel() - defer middleware.ConcurrentGoRoutines.With( - prometheus.Labels{ - "method": "NextDrainJob", - }).Dec() - - middleware.ConcurrentGoRoutines.With( - prometheus.Labels{ - "method": "NextDrainJob", - }).Inc() - - _, err := service.RunNextDrainJob(scopedCtx) - if err != nil { - sentry.CaptureException(err) - } - }() - } - - if depositProvider == "brave" && wallet.UserDepositDestination != "" { - asyncCtx, asyncCancel := context.WithTimeout(context.Background(), 90*time.Second) - scopedCtx := appctx.Wrap(ctx, asyncCtx) - - go func() { - defer asyncCancel() - defer middleware.ConcurrentGoRoutines.With( - prometheus.Labels{ - "method": "NextMintDrainJob", - }).Dec() - - middleware.ConcurrentGoRoutines.With( - prometheus.Labels{ - "method": "NextMintDrainJob", - }).Inc() - - _, err := service.RunNextMintDrainJob(scopedCtx) - if err != nil { - sentry.CaptureException(err) - } - }() - } - - return &batchID, nil -} - -// DrainPoll - Response structure for the DrainPoll -type DrainPoll struct { - ID *uuid.UUID `db:"id"` - Status string `db:"status"` -} - -// DrainWorker attempts to work on a drain job by redeeming the credentials and transferring funds -type DrainWorker interface { - RedeemAndTransferFunds(ctx context.Context, credentials []cbr.CredentialRedemption, drainJob DrainJob) (*walletutils.TransactionInfo, error) -} - -// DrainRetryWorker - reads walletID -type DrainRetryWorker interface { - FetchAdminAttestationWalletID(ctx context.Context) (*uuid.UUID, error) -} - -// MintWorker mint worker describes what a mint worker is able to do, mint grants -type MintWorker interface { - MintGrant(ctx context.Context, walletID uuid.UUID, total decimal.Decimal, promoIDs ...uuid.UUID) error -} - -// BatchTransferWorker - Worker that has the ability to "submit" a batch of transactions with payments service. -// The DrainWorker tasks employ the payments GRPC client "prepare" method, and provide the "batch id" in the -// metadata of the grpc request. Payments GRPC server will append all TXs in a batch to a single transfer job. -// The SubmitBatchTransfer will notice claim_drain batches that are complete, and perform a submit to the Payments API -type BatchTransferWorker interface { - SubmitBatchTransfer(ctx context.Context, batchID *uuid.UUID) error -} - -// GeminiTxnStatusWorker this worker retrieves the status for a given gemini transaction -type GeminiTxnStatusWorker interface { - GetGeminiTxnStatus(ctx context.Context, txRef string) (*walletutils.TransactionInfo, error) -} - -// drainClaimErred - a codified err type for draind -type drainClaimErred struct { - error - Code string -} - -// DrainCode - implement claim drain erred code -func (dce *drainClaimErred) DrainCode() (string, bool) { - return dce.Code, false -} - -var ( - errMismatchedWallet = &drainClaimErred{ - errors.New("claim does not exist for wallet"), - "mismatched_wallet", - } - errInvalidSuggestionCount = &drainClaimErred{ - errors.New("invalid number of suggestions"), - "invalid_suggestion_count", - } - errInvalidSuggestionAmount = &drainClaimErred{ - errors.New("attempting to claim more funds than earned"), - "invalid_suggestion_amount", - } - drainCodeErrorInvalidDepositID = errorutils.Codified{ - ErrCode: "invalid_deposit_id", - Retry: false, - } -) - -// bitflyerOverTransferLimit - a error bundle "codified" implemented "data" field for error bundle -// providing the specific drain code for the drain job error codification -type bitflyerOverTransferLimit struct{} - -func (botl *bitflyerOverTransferLimit) DrainCode() (string, bool) { - return "bf_transfer_limit", true -} - -// SubmitBatchTransfer after validating that all the credential bindings -func (service *Service) SubmitBatchTransfer(ctx context.Context, batchID *uuid.UUID) error { - // setup a logger - logger := logging.Logger(ctx, "promotion.SubmitBatchTransfer") - - // TODO: when nitro enablement we will perform tx submissions here - // but for now we will perform the bf client bulk upload - /* - // use paymentsClient to "prepare" transfer with batch id - _, err = service.paymentsClient.Submit(ctx, &paymentspb.SubmitRequest{ - BatchMeta: &paymentspb.BatchMeta{ - BatchId: batchID.String(), - }, - }) - if err != nil { - logger.Error().Err(err).Msg("failed to call submit to payments") - return fmt.Errorf("failed to call submit for payments transfer: %w", err) - } - */ - // for now we will only be batching bitflyer txs - - // get quote, make sure we dont go over 100K JPY - quote, err := service.bfClient.FetchQuote(ctx, "BAT_JPY", false) - if err != nil { - var eb *errorutils.ErrorBundle - if errors.As(err, &eb) { - logger.Error(). - Str("error bundle", eb.DataToString()). - Msg("failed to fetch quote") - } - // if this was a bitflyer error and the error is due to a 401 response, refresh the token - var bfe *clients.BitflyerError - if errors.As(err, &bfe) { - if bfe.HTTPStatusCode == http.StatusUnauthorized { - // try to refresh the token and go again - logger.Warn().Msg("attempting to refresh the bf token") - _, err = service.bfClient.RefreshToken(ctx, bitflyer.TokenPayloadFromCtx(ctx)) - if err != nil { - return fmt.Errorf("failed to get token from bf: %w", err) - } - // redo the request after token refresh - quote, err = service.bfClient.FetchQuote(ctx, "BAT_JPY", false) - if err != nil { - return fmt.Errorf("failed to fetch bitflyer quote: %w", err) - } - } - } else { - // unknown error - return fmt.Errorf("failed to fetch bitflyer quote: %w", err) - } - } - - JPYLimit := decimal.NewFromFloat(100000) - var overLimitErr error - - // get all transactions associated with batch id - transfers, err := service.Datastore.GetDrainsByBatchID(ctx, batchID) - if err != nil { - return fmt.Errorf("failed to get transactions for batch: %w", err) - } - var ( - withdraws = []bitflyer.WithdrawToDepositIDPayload{} - totalJPYTransfer = decimal.Zero - ) - - var ( - totalF64 float64 - depositID string - ) - - for _, v := range transfers { - - if v.DepositID == nil { - return errorutils.New(fmt.Errorf("failed depositID cannot be nil for batchID %s", batchID), - "submit batch transfer", drainCodeErrorInvalidDepositID) - } - - // set deposit id for the transfer - depositID = *v.DepositID - - t, _ := v.Total.Float64() - totalF64 += t - - totalJPYTransfer = totalJPYTransfer.Add(v.Total.Mul(quote.Rate)) - if totalJPYTransfer.GreaterThan(JPYLimit) { - over := JPYLimit.Sub(totalJPYTransfer).String() - totalF64, _ = JPYLimit.Div(quote.Rate).Floor().Float64() - overLimitErr = errorutils.New( - fmt.Errorf( - "over custodian transfer limit - JPY by %s; BAT_JPY rate: %v; BAT: %v", - over, quote.Rate, totalJPYTransfer), - "over custodian transfer limit", - new(bitflyerOverTransferLimit)) - break - } - } - - // collapse into one transaction, not multiples in a bulk upload - - withdraws = append(withdraws, bitflyer.WithdrawToDepositIDPayload{ - CurrencyCode: "BAT", - Amount: totalF64, - DepositID: depositID, - TransferID: batchID.String(), - SourceFrom: "userdrain", - }) - - // create a WithdrawToDepositIDBulkPayload - payload := bitflyer.WithdrawToDepositIDBulkPayload{ - Withdrawals: withdraws, - } - - withdrawToDepositIDBulkResponse, err := service.bfClient.UploadBulkPayout(ctx, payload) - if err != nil { - var bitflyerError *clients.BitflyerError - - switch { - case errors.As(err, &bitflyerError): - - // if this was a bitflyer 401 response refresh the token and retry upload otherwise return bitflyer error - if bitflyerError.HTTPStatusCode == http.StatusUnauthorized { - logger.Warn().Msg("attempting to refresh the bf token") - _, err = service.bfClient.RefreshToken(ctx, bitflyer.TokenPayloadFromCtx(ctx)) - if err != nil { - return fmt.Errorf("failed to get token from bf: %w", err) - } - withdrawToDepositIDBulkResponse, err = service.bfClient.UploadBulkPayout(ctx, payload) - if err != nil { - return fmt.Errorf("failed to transfer funds: %w", err) - } - } else { - return bitflyerError - } - - default: - return fmt.Errorf("failed to transfer funds: %w", err) - } - } - - if withdrawToDepositIDBulkResponse == nil || len(withdrawToDepositIDBulkResponse.Withdrawals) == 0 { - return fmt.Errorf("submit batch transfer error: response cannot be nil for batchID %s", batchID) - } - - // check the txn for errors - for _, withdrawal := range withdrawToDepositIDBulkResponse.Withdrawals { - if withdrawal.CategorizeStatus() == "failed" { - - err = fmt.Errorf("submit batch transfer error: bitflyer %s error for batchID %s", - withdrawal.Status, withdrawal.TransferID) - - retry := true - if withdrawal.Status == "NO_INV" { - retry = false - } - - codified := errorutils.Codified{ - ErrCode: fmt.Sprintf("bitflyer_%s", strings.ToLower(withdrawal.Status)), - Retry: retry, - } - - return errorutils.New(err, "submit batch transfer", codified) - } - } - - if overLimitErr != nil { - return overLimitErr - } - - return nil -} - -// RedeemAndTransferFunds after validating that all the credential bindings -func (service *Service) RedeemAndTransferFunds(ctx context.Context, credentials []cbr.CredentialRedemption, drainJob DrainJob) (*walletutils.TransactionInfo, error) { - - // setup a logger - logger := logging.Logger(ctx, "promotion.RedeemAndTransferFunds") - - wallet, err := service.wallet.Datastore.GetWallet(ctx, drainJob.WalletID) - if err != nil { - logger.Error().Err(err).Msg("RedeemAndTransferFunds: failed to get wallet") - return nil, err - } - - defer func() { - if err != nil { - custodian := "unknown" - if wallet != nil && ptr.String(wallet.UserDepositAccountProvider) != "" { - custodian = *wallet.UserDepositAccountProvider - } - countClaimDrainStatus. - With(prometheus.Labels{"custodian": custodian, - "status": "failed"}).Inc() - } - }() - - // no wallet on record - if wallet == nil { - logger.Error().Err(errorutils.ErrMissingWallet). - Msg("RedeemAndTransferFunds: missing wallet") - return nil, errorutils.ErrMissingWallet - } - // wallet not linked to deposit destination, if absent fail redeem and transfer - if wallet.UserDepositDestination == "" { - logger.Error().Err(errorutils.ErrNoDepositProviderDestination). - Msg("RedeemAndTransferFunds: no deposit provider destination") - return nil, errorutils.ErrNoDepositProviderDestination - } - if wallet.UserDepositAccountProvider == nil { - logger.Error().Msg("RedeemAndTransferFunds: no deposit provider") - return nil, errorutils.ErrNoDepositProviderDestination - } - // check to see if we skip the cbr redemption case - if skipRedeem, _ := appctx.GetBoolFromContext(ctx, appctx.SkipRedeemCredentialsCTXKey); !skipRedeem { - // failed to redeem credentials - if err = service.cbClient.RedeemCredentials(ctx, credentials, drainJob.WalletID.String()); err != nil { - logger.Error().Err(err).Msg("RedeemAndTransferFunds: failed to redeem credentials") - return nil, fmt.Errorf("failed to redeem credentials: %w", err) - } - } - - if ok, _ := appctx.GetBoolFromContext(ctx, appctx.ReputationOnDrainCTXKey); ok { - // are we running in withdrawal limit mode? - if ok, _ := appctx.GetBoolFromContext(ctx, appctx.ReputationWithdrawalOnDrainCTXKey); ok { - // tally up all prior claims on this promotion for all linked provider accounts associated - // as the "withdrawalAmount" - promotionID, withdrawalAmount, err := service.Datastore.GetWithdrawalsAssociated(&drainJob.WalletID, drainJob.ClaimID) - if err != nil { - logger.Error().Err(err).Msg("RedeemAndTransferFunds: failed to lookup associated withdrawals") - return nil, fmt.Errorf("failed to lookup associated withdrawals: %w", err) - } - - if promotionID == nil { - logger.Error().Err(err).Msg("RedeemAndTransferFunds: failed to lookup associated withdrawals") - return nil, fmt.Errorf("failed to lookup associated withdrawals: no matching promotion") - } - - // perform reputation check for wallet, and error accordingly if there is a reputation failure - reputable, cohorts, err := service.reputationClient.IsDrainReputable(ctx, drainJob.WalletID, *promotionID, withdrawalAmount) - if err != nil { - logger.Error().Err(err).Msg("RedeemAndTransferFunds: failed to check reputation of wallet") - return nil, errReputationServiceFailure - } - - if !reputable { - // use the cohort to determine the limit exceeded. - for _, cohort := range cohorts { - switch cohort { - case reputation.CohortWithdrawalLimits: - // limited withdrawal - withdrawalLimitHit.Inc() - return nil, errWalletDrainLimitExceeded - case reputation.CohortNil: - // service failure - return nil, errReputationServiceFailure - } - } - // not reputable - countDrainFlaggedUnusual.Inc() - return nil, errWalletNotReputable - } - } else { - // legacy behavior - // perform reputation check for wallet, and error accordingly if there is a reputation failure - reputable, err := service.reputationClient.IsWalletAdsReputable(ctx, drainJob.WalletID, "") - if err != nil { - logger.Error().Err(err).Msg("RedeemAndTransferFunds: failed to check reputation of wallet") - return nil, errReputationServiceFailure - } - - if !reputable { - return nil, errWalletNotReputable - } - } - } - - if *wallet.UserDepositAccountProvider == "uphold" { - // FIXME should use idempotency key - tx, err := service.hotWallet.Transfer(ctx, altcurrency.BAT, altcurrency.BAT.ToProbi(drainJob.Total), wallet.UserDepositDestination) - if err != nil { - return nil, fmt.Errorf("failed to transfer funds: %w", err) - } - if service.drainChannel != nil { - service.drainChannel <- tx - } - return tx, err - } else if *wallet.UserDepositAccountProvider == "bitflyer" { - return redeemAndTransferBitflyerFunds(ctx, service, wallet, drainJob.Total) - } else if *wallet.UserDepositAccountProvider == "gemini" { - return redeemAndTransferGeminiFunds(ctx, service, drainJob.Total, drainJob) - } else if *wallet.UserDepositAccountProvider == "brave" { - // update the mint job for this walletID - promoTotal := map[string]decimal.Decimal{} - // iterate through the credentials - // get a total count per promotion - for _, cred := range credentials { - promotionID := strings.TrimSuffix(cred.Issuer, ":control") - v, ok := promoTotal[promotionID] - if ok { - // each credential is 0.25 - promoTotal[promotionID] = v.Add(decimal.NewFromFloat(0.25)) - } else { - promoTotal[promotionID] = decimal.NewFromFloat(0.25) - } - } - for k, v := range promoTotal { - promotionID, err := uuid.FromString(k) - if err != nil { - return nil, fmt.Errorf("failed to get promotion id as uuid: %w", err) - } - // update the mint_drain_promotion table with the corresponding total redeemed - err = service.Datastore.SetMintDrainPromotionTotal(ctx, drainJob.WalletID, promotionID, v) - if err != nil { - return nil, fmt.Errorf("failed to set append total funds: %w", err) - } - } - return new(walletutils.TransactionInfo), nil - } - - logger.Error().Msg("RedeemAndTransferFunds: unknown deposit provider") - return nil, fmt.Errorf( - "failed to transfer funds: user_deposit_account_provider unknown: %s", - *wallet.UserDepositAccountProvider) -} - -func redeemAndTransferBitflyerFunds( - ctx context.Context, - service *Service, - wallet *walletutils.Info, - total decimal.Decimal, -) (*walletutils.TransactionInfo, error) { - - transferID := uuid.NewV4().String() - - tx := new(walletutils.TransactionInfo) - - tx.ID = transferID - tx.Destination = wallet.UserDepositDestination - tx.DestAmount = total - tx.Status = "bitflyer-consolidate" - - // Actual Transfer is now done in SubmitBatchTransfer worker - // job will be marked as completed - - if service.drainChannel != nil { - service.drainChannel <- tx - } - - return tx, nil -} - -func redeemAndTransferGeminiFunds(ctx context.Context, service *Service, total decimal.Decimal, drainJob DrainJob) (*walletutils.TransactionInfo, error) { - - // in the event that gemini configs or service do not exist - // error on redeem and transfer - if service.geminiConf == nil { - return nil, fmt.Errorf("missing gemini client and configuration: %w", errGeminiMisconfigured) - } - - txType := "drain" - channel := "wallet" - transferID := uuid.NewV4().String() - - depositDestination := ptr.String(drainJob.DepositDestination) - if depositDestination == "" { - return nil, fmt.Errorf("error deposit destination is nil for drain job %s", drainJob.ID.String()) - } - - tx := new(walletutils.TransactionInfo) - tx.ID = transferID - tx.Destination = depositDestination - tx.DestAmount = total - tx.Status = txnStatusGeminiPending - - settlementTx := custodian.Transaction{ - SettlementID: transferID, - Type: txType, - Destination: depositDestination, - Channel: channel, - } - - account := "primary" // the account we want to drain from - payouts := []gemini.PayoutPayload{ - { - TxRef: gemini.GenerateTxRef(&settlementTx), - Amount: total, - Currency: "BAT", - Destination: depositDestination, - Account: &account, - }, - } - - payload := gemini.NewBulkPayoutPayload( - &account, - service.geminiConf.ClientID, - &payouts, - ) - // upload - signer := cryptography.NewHMACHasher([]byte(service.geminiConf.Secret)) - serializedPayload, err := json.Marshal(payload) - if err != nil { - return nil, fmt.Errorf("failed to serialize payload: %w", err) - } - // gemini client will base64 encode the payload prior to sending - resp, err := service.geminiClient.UploadBulkPayout( - ctx, - service.geminiConf.APIKey, - signer, - string(serializedPayload), - ) - - if err != nil { - var eb *errorutils.ErrorBundle - if errors.As(err, &eb) { - // retrieve the error bundle data if there is any and log - errorData := eb.DataToString() - logging.FromContext(ctx).Error(). - Err(eb.Cause()). - Interface("wallet_id", drainJob.WalletID). - Str("error_bundle", errorData). - Msg("failed to transfer funds gemini") - } - return nil, fmt.Errorf("failed to transfer funds: %w", err) - } - - if resp == nil || len(*resp) < 1 { - // failed to get a response from the server - return nil, fmt.Errorf("failed to transfer funds: gemini 'result' is not OK") - } - - // for all the submitted, check they are all okay - for _, payout := range *resp { - - logging.FromContext(ctx).Info(). - Interface("wallet_id", drainJob.WalletID). - Str("tx_ref", payout.TxRef). - Str("payout_result", payout.Result). - Str("payout_status", ptr.StringOr(payout.Status, "unknown_status")). - Str("payout_reason", ptr.StringOr(payout.Reason, "no_reason")). - Msg("checking gemini submitted transactions") - - if strings.ToLower(payout.Result) != "ok" { - return nil, fmt.Errorf("failed to transfer funds: gemini 'result' is not OK: %s", - ptr.StringOr(payout.Reason, "unknown reason")) - } - if strings.ToLower(ptr.String(payout.Status)) == "failed" { - return nil, fmt.Errorf("failed to transfer funds: gemini payout status failed: %s", - ptr.StringOr(payout.Reason, "unknown reason")) - } - } - - // used for testing only - if service.drainChannel != nil { - service.drainChannel <- tx - } - - return tx, err -} - // MintGrant create a new grant for the wallet specified with the total specified func (service *Service) MintGrant(ctx context.Context, walletID uuid.UUID, total decimal.Decimal, promotions ...uuid.UUID) error { // setup a logger @@ -852,57 +95,3 @@ func (service *Service) FetchAdminAttestationWalletID(ctx context.Context) (*uui return &walletID, nil } - -// GetGeminiTxnStatus retrieves the status for a given gemini transaction -func (service *Service) GetGeminiTxnStatus(ctx context.Context, txRef string) (*walletutils.TransactionInfo, error) { - apiKey, ok := ctx.Value(appctx.GeminiAPIKeyCTXKey).(string) - if !ok { - return nil, fmt.Errorf("no gemini api key in ctx: %w", appctx.ErrNotInContext) - } - - clientID, ok := ctx.Value(appctx.GeminiClientIDCTXKey).(string) - if !ok { - return nil, fmt.Errorf("no gemini browser client id in ctx: %w", appctx.ErrNotInContext) - } - - payoutResult, err := service.geminiClient.CheckTxStatus(ctx, apiKey, clientID, txRef) - if err != nil { - var errorBundle *errorutils.ErrorBundle - if errors.As(err, &errorBundle) { - errorData := errorBundle.DataToString() - logging.FromContext(ctx).Error(). - Err(errorBundle.Cause()). - Str("txRef", txRef). - Str("error_bundle", errorData). - Msg("gemini client check status error") - - if httpState, ok := errorBundle.Data().(clients.HTTPState); ok { - if httpState.Status == http.StatusNotFound { - return &walletutils.TransactionInfo{Status: "failed", Note: "GEMINI_NOT_FOUND"}, nil - } - } - } - return nil, fmt.Errorf("failed to check gemini txn status for %s: %w", txRef, err) - } - - if payoutResult == nil { - return nil, fmt.Errorf("failed to get gemini txn status for %s: response nil", txRef) - } - - if strings.ToLower(payoutResult.Result) == "error" { - return nil, fmt.Errorf("failed to get gemini txn status for %s: %s", txRef, - ptr.StringOr(payoutResult.Reason, "unknown gemini response error")) - } - - switch strings.ToLower(ptr.String(payoutResult.Status)) { - case "completed": - return &walletutils.TransactionInfo{Status: "complete"}, nil - case "pending", "processing": - return &walletutils.TransactionInfo{Status: "pending"}, nil - case "failed": - return &walletutils.TransactionInfo{Status: "failed", Note: ptr.String(payoutResult.Reason)}, nil - } - - return nil, fmt.Errorf("failed to get txn status for %s: unknown status %s", - txRef, ptr.String(payoutResult.Status)) -} diff --git a/services/promotion/drain_test.go b/services/promotion/drain_test.go index 0b1b3d013..8b52434be 100644 --- a/services/promotion/drain_test.go +++ b/services/promotion/drain_test.go @@ -1,910 +1 @@ package promotion - -import ( - "context" - "encoding/json" - "errors" - "fmt" - mockdialer "github.com/brave-intl/bat-go/libs/kafka/mock" - "math/rand" - "net/http" - "testing" - "time" - - "github.com/brave-intl/bat-go/libs/clients/gemini" - mock_gemini "github.com/brave-intl/bat-go/libs/clients/gemini/mock" - - "github.com/brave-intl/bat-go/libs/clients" - appctx "github.com/brave-intl/bat-go/libs/context" - "github.com/brave-intl/bat-go/libs/ptr" - testutils "github.com/brave-intl/bat-go/libs/test" - - "github.com/brave-intl/bat-go/libs/clients/bitflyer" - mock_bitflyer "github.com/brave-intl/bat-go/libs/clients/bitflyer/mock" - errorutils "github.com/brave-intl/bat-go/libs/errors" - "github.com/brave-intl/bat-go/libs/logging" - "github.com/shopspring/decimal" - - kafkautils "github.com/brave-intl/bat-go/libs/kafka" - "github.com/golang/mock/gomock" - "github.com/linkedin/goavro" - uuid "github.com/satori/go.uuid" - "github.com/segmentio/kafka-go" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestReadMessage_KafkaError(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - kafkaReader := mockdialer.NewMockConsumer(ctrl) - - ctx := context.Background() - err := errors.New(uuid.NewV4().String()) - - kafkaReader.EXPECT(). - ReadMessage(gomock.Eq(ctx)). - Return(kafka.Message{}, err) - - s := Service{ - kafkaAdminAttestationReader: kafkaReader, - } - - expected := fmt.Errorf("read message: error reading kafka message %w", err) - - walletID, actual := s.FetchAdminAttestationWalletID(ctx) - - assert.Nil(t, walletID) - assert.EqualError(t, actual, expected.Error()) -} - -func TestReadMessage_CodecError(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - kafkaReader := mockdialer.NewMockConsumer(ctrl) - - ctx := context.Background() - - kafkaReader.EXPECT(). - ReadMessage(gomock.Eq(ctx)). - Return(kafka.Message{}, nil) - - codec := make(map[string]*goavro.Codec) - - s := Service{ - codecs: codec, - kafkaAdminAttestationReader: kafkaReader, - } - - expected := fmt.Errorf("read message: could not find codec %s", adminAttestationTopic) - - walletID, actual := s.FetchAdminAttestationWalletID(ctx) - - assert.Nil(t, walletID) - assert.EqualError(t, actual, expected.Error()) -} - -func TestReadMessage_WalletIDInvalidError(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - codecs, err := kafkautils.GenerateCodecs(map[string]string{ - adminAttestationTopic: adminAttestationEventSchema, - }) - require.NoError(t, err) - - ctx := context.Background() - msg := makeMsg() - - msg.WalletID = "invalid" - - textual, err := json.Marshal(msg) - require.NoError(t, err) - - native, _, err := codecs[adminAttestationTopic].NativeFromTextual(textual) - require.NoError(t, err) - - binary, err := codecs[adminAttestationTopic].BinaryFromNative(nil, native) - require.NoError(t, err) - - message := kafka.Message{ - Key: []byte(uuid.NewV4().String()), - Value: binary, - } - - kafkaReader := mockdialer.NewMockConsumer(ctrl) - kafkaReader.EXPECT(). - ReadMessage(gomock.Eq(ctx)). - Return(message, nil) - - s := Service{ - codecs: codecs, - kafkaAdminAttestationReader: kafkaReader, - } - - expected := fmt.Errorf("read message: error could not decode walletID %s", msg.WalletID) - - walletID, actual := s.FetchAdminAttestationWalletID(ctx) - require.NoError(t, err) - - assert.Nil(t, walletID) - assert.EqualError(t, actual, expected.Error()) -} - -func TestReadMessage_Success(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - codecs, err := kafkautils.GenerateCodecs(map[string]string{ - adminAttestationTopic: adminAttestationEventSchema, - }) - require.NoError(t, err) - - ctx := context.Background() - msg := makeMsg() - - textual, err := json.Marshal(msg) - require.NoError(t, err) - - native, _, err := codecs[adminAttestationTopic].NativeFromTextual(textual) - require.NoError(t, err) - - binary, err := codecs[adminAttestationTopic].BinaryFromNative(nil, native) - require.NoError(t, err) - - message := kafka.Message{ - Key: []byte(uuid.NewV4().String()), - Value: binary, - } - - kafkaReader := mockdialer.NewMockConsumer(ctrl) - kafkaReader.EXPECT(). - ReadMessage(gomock.Eq(ctx)). - Return(message, nil) - - s := Service{ - codecs: codecs, - kafkaAdminAttestationReader: kafkaReader, - } - - expected, err := uuid.FromString(msg.WalletID) - require.NoError(t, err) - - actual, err := s.FetchAdminAttestationWalletID(ctx) - require.NoError(t, err) - - assert.Equal(t, &expected, actual) -} - -func TestSubmitBatchTransfer_Nil_DepositDestination(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - ctx, _ := logging.SetupLogger(context.Background()) - batchID := ptr.FromUUID(uuid.NewV4()) - - quote := bitflyer.Quote{ - Rate: decimal.New(1, 1), - } - - bitFlyerClient := mock_bitflyer.NewMockClient(ctrl) - bitFlyerClient.EXPECT(). - FetchQuote(ctx, "BAT_JPY", false). - Return("e, nil) - - drainTransfers := make([]DrainTransfer, 5) - - for i := 0; i < len(drainTransfers); i++ { - depositID := ptr.FromString(uuid.NewV4().String()) - // set invalid deposit id - if i == 3 { - depositID = nil - } - drainTransfers[i] = DrainTransfer{ - ID: ptr.FromUUID(uuid.NewV4()), - Total: decimal.NewFromFloat(1), - DepositID: depositID, - } - } - - datastore := NewMockDatastore(ctrl) - datastore.EXPECT(). - GetDrainsByBatchID(ctx, batchID). - Return(drainTransfers, nil) - - s := Service{ - bfClient: bitFlyerClient, - Datastore: datastore, - } - - expected := errorutils.New(fmt.Errorf("failed depositID cannot be nil for batchID %s", batchID), - "submit batch transfer", drainCodeErrorInvalidDepositID) - - err := s.SubmitBatchTransfer(ctx, batchID) - assert.Equal(t, expected, err) -} - -func TestGetGeminiTxnStatus_Completed(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - apiKey := testutils.RandomString() - clientID := testutils.RandomString() - txRef := testutils.RandomString() - - ctx := context.Background() - ctx = context.WithValue(ctx, appctx.GeminiAPIKeyCTXKey, apiKey) - ctx = context.WithValue(ctx, appctx.GeminiClientIDCTXKey, clientID) - - response := &gemini.PayoutResult{ - Result: "Ok", - Status: ptr.FromString("Completed"), - } - - geminiClient := mock_gemini.NewMockClient(ctrl) - geminiClient.EXPECT(). - CheckTxStatus(ctx, apiKey, clientID, txRef). - Return(response, nil) - - service := Service{ - geminiClient: geminiClient, - } - - actual, err := service.GetGeminiTxnStatus(ctx, txRef) - - assert.Nil(t, err) - assert.Equal(t, "complete", actual.Status) - assert.Equal(t, "", actual.Note) -} - -func TestGetGeminiTxnStatus_Pending(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - apiKey := testutils.RandomString() - clientID := testutils.RandomString() - txRef := testutils.RandomString() - - ctx := context.Background() - ctx = context.WithValue(ctx, appctx.GeminiAPIKeyCTXKey, apiKey) - ctx = context.WithValue(ctx, appctx.GeminiClientIDCTXKey, clientID) - - response := &gemini.PayoutResult{ - Result: "Ok", - Status: ptr.FromString("Pending"), - } - - geminiClient := mock_gemini.NewMockClient(ctrl) - geminiClient.EXPECT(). - CheckTxStatus(ctx, apiKey, clientID, txRef). - Return(response, nil) - - service := Service{ - geminiClient: geminiClient, - } - - actual, err := service.GetGeminiTxnStatus(ctx, txRef) - - assert.Nil(t, err) - assert.Equal(t, "pending", actual.Status) - assert.Equal(t, "", actual.Note) -} - -func TestGetGeminiTxnStatus_Processing(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - apiKey := testutils.RandomString() - clientID := testutils.RandomString() - txRef := testutils.RandomString() - - ctx := context.Background() - ctx = context.WithValue(ctx, appctx.GeminiAPIKeyCTXKey, apiKey) - ctx = context.WithValue(ctx, appctx.GeminiClientIDCTXKey, clientID) - - response := &gemini.PayoutResult{ - Result: "Ok", - Status: ptr.FromString("Processing"), - } - - geminiClient := mock_gemini.NewMockClient(ctrl) - geminiClient.EXPECT(). - CheckTxStatus(ctx, apiKey, clientID, txRef). - Return(response, nil) - - service := Service{ - geminiClient: geminiClient, - } - - actual, err := service.GetGeminiTxnStatus(ctx, txRef) - - assert.Nil(t, err) - assert.Equal(t, "pending", actual.Status) - assert.Equal(t, "", actual.Note) -} - -func TestGetGeminiTxnStatus_Failed(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - apiKey := testutils.RandomString() - clientID := testutils.RandomString() - txRef := testutils.RandomString() - - ctx := context.Background() - ctx = context.WithValue(ctx, appctx.GeminiAPIKeyCTXKey, apiKey) - ctx = context.WithValue(ctx, appctx.GeminiClientIDCTXKey, clientID) - - response := &gemini.PayoutResult{ - Result: "Ok", - Status: ptr.FromString("Failed"), - Reason: ptr.FromString(testutils.RandomString()), - } - - geminiClient := mock_gemini.NewMockClient(ctrl) - geminiClient.EXPECT(). - CheckTxStatus(ctx, apiKey, clientID, txRef). - Return(response, nil) - - service := Service{ - geminiClient: geminiClient, - } - - actual, err := service.GetGeminiTxnStatus(ctx, txRef) - - assert.Nil(t, err) - assert.Equal(t, "failed", actual.Status) - assert.Equal(t, *response.Reason, actual.Note) -} - -func TestGetGeminiTxnStatus_Unknown(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - apiKey := testutils.RandomString() - clientID := testutils.RandomString() - txRef := testutils.RandomString() - - ctx := context.Background() - ctx = context.WithValue(ctx, appctx.GeminiAPIKeyCTXKey, apiKey) - ctx = context.WithValue(ctx, appctx.GeminiClientIDCTXKey, clientID) - - response := &gemini.PayoutResult{ - Result: "Ok", - Status: ptr.FromString(testutils.RandomString()), - } - - geminiClient := mock_gemini.NewMockClient(ctrl) - geminiClient.EXPECT(). - CheckTxStatus(ctx, apiKey, clientID, txRef). - Return(response, nil) - - service := Service{ - geminiClient: geminiClient, - } - - actual, err := service.GetGeminiTxnStatus(ctx, txRef) - - assert.Nil(t, actual) - assert.Error(t, err, fmt.Errorf("failed to get txn status for %s: unknown status %s", - txRef, ptr.String(response.Status)).Error()) -} - -func TestGetGeminiTxnStatus_Response_Nil(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - apiKey := testutils.RandomString() - clientID := testutils.RandomString() - txRef := testutils.RandomString() - - ctx := context.Background() - ctx = context.WithValue(ctx, appctx.GeminiAPIKeyCTXKey, apiKey) - ctx = context.WithValue(ctx, appctx.GeminiClientIDCTXKey, clientID) - - geminiClient := mock_gemini.NewMockClient(ctrl) - geminiClient.EXPECT(). - CheckTxStatus(ctx, apiKey, clientID, txRef). - Return(nil, nil) - - service := Service{ - geminiClient: geminiClient, - } - - actual, err := service.GetGeminiTxnStatus(ctx, txRef) - - assert.Nil(t, actual) - assert.EqualError(t, err, fmt.Errorf("failed to get gemini txn status for %s: response nil", txRef).Error()) -} - -func TestGetGeminiTxnStatus_CheckStatus_Error(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - apiKey := testutils.RandomString() - clientID := testutils.RandomString() - txRef := testutils.RandomString() - - ctx := context.Background() - ctx = context.WithValue(ctx, appctx.GeminiAPIKeyCTXKey, apiKey) - ctx = context.WithValue(ctx, appctx.GeminiClientIDCTXKey, clientID) - - clientError := errors.New(testutils.RandomString()) - - geminiClient := mock_gemini.NewMockClient(ctrl) - geminiClient.EXPECT(). - CheckTxStatus(ctx, apiKey, clientID, txRef). - Return(nil, clientError) - - service := Service{ - geminiClient: geminiClient, - } - - actual, err := service.GetGeminiTxnStatus(ctx, txRef) - - assert.Nil(t, actual) - assert.EqualError(t, err, fmt.Errorf("failed to check gemini txn status for %s: %w", txRef, clientError).Error()) -} - -func TestGetGeminiTxnStatus_CheckStatus_ErrorBundle(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - apiKey := testutils.RandomString() - clientID := testutils.RandomString() - txRef := testutils.RandomString() - - ctx := context.Background() - ctx = context.WithValue(ctx, appctx.GeminiAPIKeyCTXKey, apiKey) - ctx = context.WithValue(ctx, appctx.GeminiClientIDCTXKey, clientID) - - header := http.Header{} - header.Add(testutils.RandomString(), testutils.RandomString()) - header.Add(testutils.RandomString(), testutils.RandomString()) - - path := testutils.RandomString() - status := http.StatusInternalServerError - message := testutils.RandomString() - errorData := struct { - ResponseHeaders interface{} - Body interface{} - }{ - ResponseHeaders: header, - Body: testutils.RandomString(), - } - - wrappedError := errors.New(testutils.RandomString()) - - errorBundle := clients.NewHTTPError(wrappedError, path, message, status, errorData) - - clientError := fmt.Errorf("client error %w", errorBundle) - - geminiClient := mock_gemini.NewMockClient(ctrl) - geminiClient.EXPECT(). - CheckTxStatus(ctx, apiKey, clientID, txRef). - Return(nil, clientError) - - service := Service{ - geminiClient: geminiClient, - } - - actual, err := service.GetGeminiTxnStatus(ctx, txRef) - - assert.Nil(t, actual) - assert.EqualError(t, err, fmt.Errorf("failed to check gemini txn status for %s: %w", txRef, clientError).Error()) -} - -func TestGetGeminiTxnStatus_CheckStatus_ErrorBundle_StatusNotFound(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - apiKey := testutils.RandomString() - clientID := testutils.RandomString() - txRef := testutils.RandomString() - - ctx := context.Background() - ctx = context.WithValue(ctx, appctx.GeminiAPIKeyCTXKey, apiKey) - ctx = context.WithValue(ctx, appctx.GeminiClientIDCTXKey, clientID) - - header := http.Header{} - header.Add(testutils.RandomString(), testutils.RandomString()) - header.Add(testutils.RandomString(), testutils.RandomString()) - - path := testutils.RandomString() - status := http.StatusNotFound - message := testutils.RandomString() - errorData := struct { - ResponseHeaders interface{} - Body interface{} - }{ - ResponseHeaders: header, - Body: testutils.RandomString(), - } - - wrappedError := errors.New(testutils.RandomString()) - - errorBundle := clients.NewHTTPError(wrappedError, path, message, status, errorData) - - clientError := fmt.Errorf("client error %w", errorBundle) - - geminiClient := mock_gemini.NewMockClient(ctrl) - geminiClient.EXPECT(). - CheckTxStatus(ctx, apiKey, clientID, txRef). - Return(nil, clientError) - - service := Service{ - geminiClient: geminiClient, - } - - actual, err := service.GetGeminiTxnStatus(ctx, txRef) - - assert.Nil(t, err) - assert.Equal(t, "failed", actual.Status) - assert.Equal(t, "GEMINI_NOT_FOUND", actual.Note) -} - -func TestGetGeminiTxnStatus_ResponseError_NoReason(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - apiKey := testutils.RandomString() - clientID := testutils.RandomString() - txRef := testutils.RandomString() - - ctx := context.Background() - ctx = context.WithValue(ctx, appctx.GeminiAPIKeyCTXKey, apiKey) - ctx = context.WithValue(ctx, appctx.GeminiClientIDCTXKey, clientID) - - response := &gemini.PayoutResult{ - Result: "Error", - } - - geminiClient := mock_gemini.NewMockClient(ctrl) - geminiClient.EXPECT(). - CheckTxStatus(ctx, apiKey, clientID, txRef). - Return(response, nil) - - service := Service{ - geminiClient: geminiClient, - } - - actual, err := service.GetGeminiTxnStatus(ctx, txRef) - - assert.Nil(t, actual) - assert.EqualError(t, err, fmt.Errorf("failed to get gemini txn status for %s: unknown gemini response error", txRef).Error()) -} - -func TestGetGeminiTxnStatus_ResponseError_WithReason(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - apiKey := testutils.RandomString() - clientID := testutils.RandomString() - txRef := testutils.RandomString() - - ctx := context.Background() - ctx = context.WithValue(ctx, appctx.GeminiAPIKeyCTXKey, apiKey) - ctx = context.WithValue(ctx, appctx.GeminiClientIDCTXKey, clientID) - - response := &gemini.PayoutResult{ - Result: "Error", - Reason: ptr.FromString(testutils.RandomString()), - } - - geminiClient := mock_gemini.NewMockClient(ctrl) - geminiClient.EXPECT(). - CheckTxStatus(ctx, apiKey, clientID, txRef). - Return(response, nil) - - service := Service{ - geminiClient: geminiClient, - } - - actual, err := service.GetGeminiTxnStatus(ctx, txRef) - - assert.Nil(t, actual) - assert.EqualError(t, err, fmt.Errorf("failed to get gemini txn status for %s: %s", txRef, *response.Reason).Error()) -} - -func TestSubmitBatchTransfer_UploadBulkPayout_NOINV(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - ctx, _ := logging.SetupLogger(context.Background()) - batchID := ptr.FromUUID(uuid.NewV4()) - - quote := bitflyer.Quote{ - Rate: decimal.New(1, 1), - } - - bfClient := mock_bitflyer.NewMockClient(ctrl) - bfClient.EXPECT(). - FetchQuote(ctx, "BAT_JPY", false). - Return("e, nil) - - drainTransfers := make([]DrainTransfer, 1) - drainTransfers[0] = DrainTransfer{ - ID: ptr.FromUUID(uuid.NewV4()), - Total: decimal.NewFromFloat(1), - DepositID: ptr.FromString(uuid.NewV4().String()), - } - - datastore := NewMockDatastore(ctrl) - datastore.EXPECT(). - GetDrainsByBatchID(ctx, batchID). - Return(drainTransfers, nil) - - var bitflyerError = new(clients.BitflyerError) - bitflyerError.HTTPStatusCode = http.StatusUnauthorized - - bfClient.EXPECT(). - UploadBulkPayout(ctx, gomock.Any()). - Return(nil, bitflyerError) - - bfClient.EXPECT(). - RefreshToken(ctx, gomock.Any()). - Return(nil, nil) - - withdrawal := bitflyer.WithdrawToDepositIDResponse{ - Status: "NO_INV", - } - - withdrawToDepositIDBulkResponse := bitflyer.WithdrawToDepositIDBulkResponse{ - DryRun: false, - Withdrawals: []bitflyer.WithdrawToDepositIDResponse{ - withdrawal, - }, - } - - bfClient.EXPECT(). - UploadBulkPayout(ctx, gomock.Any()). - Return(&withdrawToDepositIDBulkResponse, nil) - - s := Service{ - bfClient: bfClient, - Datastore: datastore, - } - - err := fmt.Errorf("submit batch transfer error: bitflyer %s error for batchID %s", - withdrawal.Status, withdrawal.TransferID) - - codified := errorutils.Codified{ - ErrCode: "bitflyer_no_inv", - Retry: false, - } - - expected := errorutils.New(err, "submit batch transfer", codified) - actual := s.SubmitBatchTransfer(ctx, batchID) - - assert.Equal(t, expected, actual) -} - -func TestSubmitBatchTransfer_UploadBulkPayout_Error(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - ctx, _ := logging.SetupLogger(context.Background()) - batchID := ptr.FromUUID(uuid.NewV4()) - - quote := bitflyer.Quote{ - Rate: decimal.New(1, 1), - } - - bfClient := mock_bitflyer.NewMockClient(ctrl) - bfClient.EXPECT(). - FetchQuote(ctx, "BAT_JPY", false). - Return("e, nil) - - drainTransfers := make([]DrainTransfer, 1) - drainTransfers[0] = DrainTransfer{ - ID: ptr.FromUUID(uuid.NewV4()), - Total: decimal.NewFromFloat(1), - DepositID: ptr.FromString(uuid.NewV4().String()), - } - - datastore := NewMockDatastore(ctrl) - datastore.EXPECT(). - GetDrainsByBatchID(ctx, batchID). - Return(drainTransfers, nil) - - var bitflyerError = new(clients.BitflyerError) - bitflyerError.HTTPStatusCode = http.StatusUnauthorized - - err := errors.New("some error") - - bfClient.EXPECT(). - UploadBulkPayout(ctx, gomock.Any()). - Return(nil, err) - - s := Service{ - bfClient: bfClient, - Datastore: datastore, - } - - actual := s.SubmitBatchTransfer(ctx, batchID) - - assert.EqualError(t, actual, fmt.Sprintf("failed to transfer funds: %s", err.Error())) -} - -func TestSubmitBatchTransfer_UploadBulkPayout_Bitflyer_Unauthorized_Retry(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - ctx, _ := logging.SetupLogger(context.Background()) - batchID := ptr.FromUUID(uuid.NewV4()) - - quote := bitflyer.Quote{ - Rate: decimal.New(1, 1), - } - - bfClient := mock_bitflyer.NewMockClient(ctrl) - bfClient.EXPECT(). - FetchQuote(ctx, "BAT_JPY", false). - Return("e, nil) - - drainTransfers := make([]DrainTransfer, 1) - drainTransfers[0] = DrainTransfer{ - ID: ptr.FromUUID(uuid.NewV4()), - Total: decimal.NewFromFloat(1), - DepositID: ptr.FromString(uuid.NewV4().String()), - } - - datastore := NewMockDatastore(ctrl) - datastore.EXPECT(). - GetDrainsByBatchID(ctx, batchID). - Return(drainTransfers, nil) - - var bitflyerError = new(clients.BitflyerError) - bitflyerError.HTTPStatusCode = http.StatusUnauthorized - - bfClient.EXPECT(). - UploadBulkPayout(ctx, gomock.Any()). - Return(nil, bitflyerError) - - bfClient.EXPECT(). - RefreshToken(ctx, gomock.Any()). - Return(nil, nil) - - withdrawToDepositIDBulkResponse := bitflyer.WithdrawToDepositIDBulkResponse{ - DryRun: false, - Withdrawals: []bitflyer.WithdrawToDepositIDResponse{ - { - Status: "SUCCESS", - }, - }, - } - - bfClient.EXPECT(). - UploadBulkPayout(ctx, gomock.Any()). - Return(&withdrawToDepositIDBulkResponse, nil) - - s := Service{ - bfClient: bfClient, - Datastore: datastore, - } - - err := s.SubmitBatchTransfer(ctx, batchID) - assert.Nil(t, err) -} - -func TestSubmitBatchTransfer_UploadBulkPayout_Bitflyer_Unauthorized_NoRetry(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - ctx, _ := logging.SetupLogger(context.Background()) - batchID := ptr.FromUUID(uuid.NewV4()) - - quote := bitflyer.Quote{ - Rate: decimal.New(1, 1), - } - - bfClient := mock_bitflyer.NewMockClient(ctrl) - bfClient.EXPECT(). - FetchQuote(ctx, "BAT_JPY", false). - Return("e, nil) - - drainTransfers := make([]DrainTransfer, 1) - drainTransfers[0] = DrainTransfer{ - ID: ptr.FromUUID(uuid.NewV4()), - Total: decimal.NewFromFloat(1), - DepositID: ptr.FromString(uuid.NewV4().String()), - } - - datastore := NewMockDatastore(ctrl) - datastore.EXPECT(). - GetDrainsByBatchID(ctx, batchID). - Return(drainTransfers, nil) - - var bitflyerError = new(clients.BitflyerError) - bitflyerError.HTTPStatusCode = http.StatusUnauthorized - - bfClient.EXPECT(). - UploadBulkPayout(ctx, gomock.Any()). - Return(nil, bitflyerError) - - refreshTokenError := errors.New("some error") - bfClient.EXPECT(). - RefreshToken(ctx, gomock.Any()). - Return(nil, refreshTokenError) - - s := Service{ - bfClient: bfClient, - Datastore: datastore, - } - - err := s.SubmitBatchTransfer(ctx, batchID) - - assert.EqualError(t, err, fmt.Errorf("failed to get token from bf: %w", refreshTokenError).Error()) -} - -func TestSubmitBatchTransfer_UploadBulkPayout_Bitflyer_NoWithdrawals(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - ctx, _ := logging.SetupLogger(context.Background()) - batchID := ptr.FromUUID(uuid.NewV4()) - - quote := bitflyer.Quote{ - Rate: decimal.New(1, 1), - } - - bfClient := mock_bitflyer.NewMockClient(ctrl) - bfClient.EXPECT(). - FetchQuote(ctx, "BAT_JPY", false). - Return("e, nil) - - drainTransfers := make([]DrainTransfer, 1) - drainTransfers[0] = DrainTransfer{ - ID: ptr.FromUUID(uuid.NewV4()), - Total: decimal.NewFromFloat(1), - DepositID: ptr.FromString(uuid.NewV4().String()), - } - - datastore := NewMockDatastore(ctrl) - datastore.EXPECT(). - GetDrainsByBatchID(ctx, batchID). - Return(drainTransfers, nil) - - var bitflyerError = new(clients.BitflyerError) - bitflyerError.HTTPStatusCode = http.StatusUnauthorized - - bfClient.EXPECT(). - UploadBulkPayout(ctx, gomock.Any()). - Return(nil, bitflyerError) - - bfClient.EXPECT(). - RefreshToken(ctx, gomock.Any()). - Return(nil, nil) - - // no withdraws - withdrawToDepositIDBulkResponse := bitflyer.WithdrawToDepositIDBulkResponse{ - DryRun: false, - Withdrawals: []bitflyer.WithdrawToDepositIDResponse{}, - } - - bfClient.EXPECT(). - UploadBulkPayout(ctx, gomock.Any()). - Return(&withdrawToDepositIDBulkResponse, nil) - - s := Service{ - bfClient: bfClient, - Datastore: datastore, - } - - err := s.SubmitBatchTransfer(ctx, batchID) - - assert.EqualError(t, err, fmt.Sprintf("submit batch transfer error: response cannot be nil for batchID %s", batchID)) -} - -func makeMsg() AdminAttestationEvent { - return AdminAttestationEvent{ - WalletID: uuid.NewV4().String(), - Service: uuid.NewV4().String(), - Signal: uuid.NewV4().String(), - Score: rand.Int31n(10), - Justification: uuid.NewV4().String(), - CreatedAt: time.Now().String(), - } -} diff --git a/services/promotion/instrumented_datastore.go b/services/promotion/instrumented_datastore.go index 6957fba13..3a25f69bd 100755 --- a/services/promotion/instrumented_datastore.go +++ b/services/promotion/instrumented_datastore.go @@ -1,9 +1,9 @@ +package promotion + // Code generated by gowrap. DO NOT EDIT. // template: ../../.prom-gowrap.tmpl // gowrap: http://github.com/hexdigest/gowrap -package promotion - //go:generate gowrap gen -p github.com/brave-intl/bat-go/services/promotion -i Datastore -t ../../.prom-gowrap.tmpl -o instrumented_datastore.go -l "" import ( @@ -11,7 +11,6 @@ import ( "time" "github.com/brave-intl/bat-go/libs/clients/cbr" - errorutils "github.com/brave-intl/bat-go/libs/errors" "github.com/brave-intl/bat-go/libs/jsonutils" walletutils "github.com/brave-intl/bat-go/libs/wallet" migrate "github.com/golang-migrate/migrate/v4" @@ -144,48 +143,6 @@ func (_d DatastoreWithPrometheus) DeactivatePromotion(promotion *Promotion) (err return _d.base.DeactivatePromotion(promotion) } -// DrainClaim implements Datastore -func (_d DatastoreWithPrometheus) DrainClaim(drainID *uuid.UUID, claim *Claim, credentials []cbr.CredentialRedemption, wallet *walletutils.Info, total decimal.Decimal, codedErr errorutils.DrainCodified) (err error) { - _since := time.Now() - defer func() { - result := "ok" - if err != nil { - result = "error" - } - - datastoreDurationSummaryVec.WithLabelValues(_d.instanceName, "DrainClaim", result).Observe(time.Since(_since).Seconds()) - }() - return _d.base.DrainClaim(drainID, claim, credentials, wallet, total, codedErr) -} - -// DrainClaims implements Datastore -func (_d DatastoreWithPrometheus) DrainClaims(drainClaims []DrainClaim) (err error) { - _since := time.Now() - defer func() { - result := "ok" - if err != nil { - result = "error" - } - - datastoreDurationSummaryVec.WithLabelValues(_d.instanceName, "DrainClaims", result).Observe(time.Since(_since).Seconds()) - }() - return _d.base.DrainClaims(drainClaims) -} - -// EnqueueMintDrainJob implements Datastore -func (_d DatastoreWithPrometheus) EnqueueMintDrainJob(ctx context.Context, walletID uuid.UUID, promotionIDs ...uuid.UUID) (err error) { - _since := time.Now() - defer func() { - result := "ok" - if err != nil { - result = "error" - } - - datastoreDurationSummaryVec.WithLabelValues(_d.instanceName, "EnqueueMintDrainJob", result).Observe(time.Since(_since).Seconds()) - }() - return _d.base.EnqueueMintDrainJob(ctx, walletID, promotionIDs...) -} - // GetAvailablePromotions implements Datastore func (_d DatastoreWithPrometheus) GetAvailablePromotions(platform string) (pa1 []Promotion, err error) { _since := time.Now() @@ -256,48 +213,6 @@ func (_d DatastoreWithPrometheus) GetClaimSummary(walletID uuid.UUID, grantType return _d.base.GetClaimSummary(walletID, grantType) } -// GetCustodianDrainInfo implements Datastore -func (_d DatastoreWithPrometheus) GetCustodianDrainInfo(paymentID *uuid.UUID) (ca1 []CustodianDrain, err error) { - _since := time.Now() - defer func() { - result := "ok" - if err != nil { - result = "error" - } - - datastoreDurationSummaryVec.WithLabelValues(_d.instanceName, "GetCustodianDrainInfo", result).Observe(time.Since(_since).Seconds()) - }() - return _d.base.GetCustodianDrainInfo(paymentID) -} - -// GetDrainPoll implements Datastore -func (_d DatastoreWithPrometheus) GetDrainPoll(drainID *uuid.UUID) (dp1 *DrainPoll, err error) { - _since := time.Now() - defer func() { - result := "ok" - if err != nil { - result = "error" - } - - datastoreDurationSummaryVec.WithLabelValues(_d.instanceName, "GetDrainPoll", result).Observe(time.Since(_since).Seconds()) - }() - return _d.base.GetDrainPoll(drainID) -} - -// GetDrainsByBatchID implements Datastore -func (_d DatastoreWithPrometheus) GetDrainsByBatchID(ctx context.Context, batchID *uuid.UUID) (da1 []DrainTransfer, err error) { - _since := time.Now() - defer func() { - result := "ok" - if err != nil { - result = "error" - } - - datastoreDurationSummaryVec.WithLabelValues(_d.instanceName, "GetDrainsByBatchID", result).Observe(time.Since(_since).Seconds()) - }() - return _d.base.GetDrainsByBatchID(ctx, batchID) -} - // GetIssuer implements Datastore func (_d DatastoreWithPrometheus) GetIssuer(promotionID uuid.UUID, cohort string) (ip1 *Issuer, err error) { _since := time.Now() @@ -543,20 +458,6 @@ func (_d DatastoreWithPrometheus) RollbackTxAndHandle(tx *sqlx.Tx) (err error) { return _d.base.RollbackTxAndHandle(tx) } -// RunNextBatchPaymentsJob implements Datastore -func (_d DatastoreWithPrometheus) RunNextBatchPaymentsJob(ctx context.Context, worker BatchTransferWorker) (b1 bool, err error) { - _since := time.Now() - defer func() { - result := "ok" - if err != nil { - result = "error" - } - - datastoreDurationSummaryVec.WithLabelValues(_d.instanceName, "RunNextBatchPaymentsJob", result).Observe(time.Since(_since).Seconds()) - }() - return _d.base.RunNextBatchPaymentsJob(ctx, worker) -} - // RunNextClaimJob implements Datastore func (_d DatastoreWithPrometheus) RunNextClaimJob(ctx context.Context, worker ClaimWorker) (b1 bool, err error) { _since := time.Now() @@ -571,62 +472,6 @@ func (_d DatastoreWithPrometheus) RunNextClaimJob(ctx context.Context, worker Cl return _d.base.RunNextClaimJob(ctx, worker) } -// RunNextDrainJob implements Datastore -func (_d DatastoreWithPrometheus) RunNextDrainJob(ctx context.Context, worker DrainWorker) (b1 bool, err error) { - _since := time.Now() - defer func() { - result := "ok" - if err != nil { - result = "error" - } - - datastoreDurationSummaryVec.WithLabelValues(_d.instanceName, "RunNextDrainJob", result).Observe(time.Since(_since).Seconds()) - }() - return _d.base.RunNextDrainJob(ctx, worker) -} - -// RunNextDrainRetryJob implements Datastore -func (_d DatastoreWithPrometheus) RunNextDrainRetryJob(ctx context.Context, worker DrainRetryWorker) (err error) { - _since := time.Now() - defer func() { - result := "ok" - if err != nil { - result = "error" - } - - datastoreDurationSummaryVec.WithLabelValues(_d.instanceName, "RunNextDrainRetryJob", result).Observe(time.Since(_since).Seconds()) - }() - return _d.base.RunNextDrainRetryJob(ctx, worker) -} - -// RunNextGeminiCheckStatus implements Datastore -func (_d DatastoreWithPrometheus) RunNextGeminiCheckStatus(ctx context.Context, worker GeminiTxnStatusWorker) (b1 bool, err error) { - _since := time.Now() - defer func() { - result := "ok" - if err != nil { - result = "error" - } - - datastoreDurationSummaryVec.WithLabelValues(_d.instanceName, "RunNextGeminiCheckStatus", result).Observe(time.Since(_since).Seconds()) - }() - return _d.base.RunNextGeminiCheckStatus(ctx, worker) -} - -// RunNextMintDrainJob implements Datastore -func (_d DatastoreWithPrometheus) RunNextMintDrainJob(ctx context.Context, worker MintWorker) (b1 bool, err error) { - _since := time.Now() - defer func() { - result := "ok" - if err != nil { - result = "error" - } - - datastoreDurationSummaryVec.WithLabelValues(_d.instanceName, "RunNextMintDrainJob", result).Observe(time.Since(_since).Seconds()) - }() - return _d.base.RunNextMintDrainJob(ctx, worker) -} - // RunNextSuggestionJob implements Datastore func (_d DatastoreWithPrometheus) RunNextSuggestionJob(ctx context.Context, worker SuggestionWorker) (b1 bool, err error) { _since := time.Now() @@ -655,34 +500,6 @@ func (_d DatastoreWithPrometheus) SaveClaimCreds(claimCreds *ClaimCreds) (err er return _d.base.SaveClaimCreds(claimCreds) } -// SetMintDrainPromotionTotal implements Datastore -func (_d DatastoreWithPrometheus) SetMintDrainPromotionTotal(ctx context.Context, walletID uuid.UUID, promotionID uuid.UUID, total decimal.Decimal) (err error) { - _since := time.Now() - defer func() { - result := "ok" - if err != nil { - result = "error" - } - - datastoreDurationSummaryVec.WithLabelValues(_d.instanceName, "SetMintDrainPromotionTotal", result).Observe(time.Since(_since).Seconds()) - }() - return _d.base.SetMintDrainPromotionTotal(ctx, walletID, promotionID, total) -} - -// UpdateDrainJobAsRetriable implements Datastore -func (_d DatastoreWithPrometheus) UpdateDrainJobAsRetriable(ctx context.Context, walletID uuid.UUID) (err error) { - _since := time.Now() - defer func() { - result := "ok" - if err != nil { - result = "error" - } - - datastoreDurationSummaryVec.WithLabelValues(_d.instanceName, "UpdateDrainJobAsRetriable", result).Observe(time.Since(_since).Seconds()) - }() - return _d.base.UpdateDrainJobAsRetriable(ctx, walletID) -} - // UpdateOrder implements Datastore func (_d DatastoreWithPrometheus) UpdateOrder(orderID uuid.UUID, status string) (err error) { _since := time.Now() diff --git a/services/promotion/instrumented_read_only_datastore.go b/services/promotion/instrumented_read_only_datastore.go index 35622b07c..95fa5ff69 100644 --- a/services/promotion/instrumented_read_only_datastore.go +++ b/services/promotion/instrumented_read_only_datastore.go @@ -1,13 +1,12 @@ +package promotion + // Code generated by gowrap. DO NOT EDIT. // template: ../../.prom-gowrap.tmpl // gowrap: http://github.com/hexdigest/gowrap -package promotion - //go:generate gowrap gen -p github.com/brave-intl/bat-go/services/promotion -i ReadOnlyDatastore -t ../../.prom-gowrap.tmpl -o instrumented_read_only_datastore.go -l "" import ( - "context" "time" walletutils "github.com/brave-intl/bat-go/libs/wallet" @@ -127,48 +126,6 @@ func (_d ReadOnlyDatastoreWithPrometheus) GetClaimSummary(walletID uuid.UUID, gr return _d.base.GetClaimSummary(walletID, grantType) } -// GetCustodianDrainInfo implements ReadOnlyDatastore -func (_d ReadOnlyDatastoreWithPrometheus) GetCustodianDrainInfo(paymentID *uuid.UUID) (ca1 []CustodianDrain, err error) { - _since := time.Now() - defer func() { - result := "ok" - if err != nil { - result = "error" - } - - readonlydatastoreDurationSummaryVec.WithLabelValues(_d.instanceName, "GetCustodianDrainInfo", result).Observe(time.Since(_since).Seconds()) - }() - return _d.base.GetCustodianDrainInfo(paymentID) -} - -// GetDrainPoll implements ReadOnlyDatastore -func (_d ReadOnlyDatastoreWithPrometheus) GetDrainPoll(drainID *uuid.UUID) (dp1 *DrainPoll, err error) { - _since := time.Now() - defer func() { - result := "ok" - if err != nil { - result = "error" - } - - readonlydatastoreDurationSummaryVec.WithLabelValues(_d.instanceName, "GetDrainPoll", result).Observe(time.Since(_since).Seconds()) - }() - return _d.base.GetDrainPoll(drainID) -} - -// GetDrainsByBatchID implements ReadOnlyDatastore -func (_d ReadOnlyDatastoreWithPrometheus) GetDrainsByBatchID(ctx context.Context, batchID *uuid.UUID) (da1 []DrainTransfer, err error) { - _since := time.Now() - defer func() { - result := "ok" - if err != nil { - result = "error" - } - - readonlydatastoreDurationSummaryVec.WithLabelValues(_d.instanceName, "GetDrainsByBatchID", result).Observe(time.Since(_since).Seconds()) - }() - return _d.base.GetDrainsByBatchID(ctx, batchID) -} - // GetIssuer implements ReadOnlyDatastore func (_d ReadOnlyDatastoreWithPrometheus) GetIssuer(promotionID uuid.UUID, cohort string) (ip1 *Issuer, err error) { _since := time.Now() diff --git a/services/promotion/mockdatastore.go b/services/promotion/mockdatastore.go index 5b110961e..db81742df 100644 --- a/services/promotion/mockdatastore.go +++ b/services/promotion/mockdatastore.go @@ -9,7 +9,6 @@ import ( reflect "reflect" cbr "github.com/brave-intl/bat-go/libs/clients/cbr" - errors "github.com/brave-intl/bat-go/libs/errors" jsonutils "github.com/brave-intl/bat-go/libs/jsonutils" wallet "github.com/brave-intl/bat-go/libs/wallet" v4 "github.com/golang-migrate/migrate/v4" @@ -145,53 +144,6 @@ func (mr *MockDatastoreMockRecorder) DeactivatePromotion(promotion interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeactivatePromotion", reflect.TypeOf((*MockDatastore)(nil).DeactivatePromotion), promotion) } -// DrainClaim mocks base method. -func (m *MockDatastore) DrainClaim(drainID *go_uuid.UUID, claim *Claim, credentials []cbr.CredentialRedemption, wallet *wallet.Info, total decimal.Decimal, codedErr errors.DrainCodified) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DrainClaim", drainID, claim, credentials, wallet, total, codedErr) - ret0, _ := ret[0].(error) - return ret0 -} - -// DrainClaim indicates an expected call of DrainClaim. -func (mr *MockDatastoreMockRecorder) DrainClaim(drainID, claim, credentials, wallet, total, codedErr interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DrainClaim", reflect.TypeOf((*MockDatastore)(nil).DrainClaim), drainID, claim, credentials, wallet, total, codedErr) -} - -// DrainClaims mocks base method. -func (m *MockDatastore) DrainClaims(drainClaims []DrainClaim) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DrainClaims", drainClaims) - ret0, _ := ret[0].(error) - return ret0 -} - -// DrainClaims indicates an expected call of DrainClaims. -func (mr *MockDatastoreMockRecorder) DrainClaims(drainClaims interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DrainClaims", reflect.TypeOf((*MockDatastore)(nil).DrainClaims), drainClaims) -} - -// EnqueueMintDrainJob mocks base method. -func (m *MockDatastore) EnqueueMintDrainJob(ctx context.Context, walletID go_uuid.UUID, promotionIDs ...go_uuid.UUID) error { - m.ctrl.T.Helper() - varargs := []interface{}{ctx, walletID} - for _, a := range promotionIDs { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "EnqueueMintDrainJob", varargs...) - ret0, _ := ret[0].(error) - return ret0 -} - -// EnqueueMintDrainJob indicates an expected call of EnqueueMintDrainJob. -func (mr *MockDatastoreMockRecorder) EnqueueMintDrainJob(ctx, walletID interface{}, promotionIDs ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{ctx, walletID}, promotionIDs...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnqueueMintDrainJob", reflect.TypeOf((*MockDatastore)(nil).EnqueueMintDrainJob), varargs...) -} - // GetAvailablePromotions mocks base method. func (m *MockDatastore) GetAvailablePromotions(platform string) ([]Promotion, error) { m.ctrl.T.Helper() @@ -267,51 +219,6 @@ func (mr *MockDatastoreMockRecorder) GetClaimSummary(walletID, grantType interfa return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClaimSummary", reflect.TypeOf((*MockDatastore)(nil).GetClaimSummary), walletID, grantType) } -// GetCustodianDrainInfo mocks base method. -func (m *MockDatastore) GetCustodianDrainInfo(paymentID *go_uuid.UUID) ([]CustodianDrain, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCustodianDrainInfo", paymentID) - ret0, _ := ret[0].([]CustodianDrain) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetCustodianDrainInfo indicates an expected call of GetCustodianDrainInfo. -func (mr *MockDatastoreMockRecorder) GetCustodianDrainInfo(paymentID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCustodianDrainInfo", reflect.TypeOf((*MockDatastore)(nil).GetCustodianDrainInfo), paymentID) -} - -// GetDrainPoll mocks base method. -func (m *MockDatastore) GetDrainPoll(drainID *go_uuid.UUID) (*DrainPoll, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDrainPoll", drainID) - ret0, _ := ret[0].(*DrainPoll) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetDrainPoll indicates an expected call of GetDrainPoll. -func (mr *MockDatastoreMockRecorder) GetDrainPoll(drainID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDrainPoll", reflect.TypeOf((*MockDatastore)(nil).GetDrainPoll), drainID) -} - -// GetDrainsByBatchID mocks base method. -func (m *MockDatastore) GetDrainsByBatchID(ctx context.Context, batchID *go_uuid.UUID) ([]DrainTransfer, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDrainsByBatchID", ctx, batchID) - ret0, _ := ret[0].([]DrainTransfer) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetDrainsByBatchID indicates an expected call of GetDrainsByBatchID. -func (mr *MockDatastoreMockRecorder) GetDrainsByBatchID(ctx, batchID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDrainsByBatchID", reflect.TypeOf((*MockDatastore)(nil).GetDrainsByBatchID), ctx, batchID) -} - // GetIssuer mocks base method. func (m *MockDatastore) GetIssuer(promotionID go_uuid.UUID, cohort string) (*Issuer, error) { m.ctrl.T.Helper() @@ -579,21 +486,6 @@ func (mr *MockDatastoreMockRecorder) RollbackTxAndHandle(tx interface{}) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RollbackTxAndHandle", reflect.TypeOf((*MockDatastore)(nil).RollbackTxAndHandle), tx) } -// RunNextBatchPaymentsJob mocks base method. -func (m *MockDatastore) RunNextBatchPaymentsJob(ctx context.Context, worker BatchTransferWorker) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RunNextBatchPaymentsJob", ctx, worker) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// RunNextBatchPaymentsJob indicates an expected call of RunNextBatchPaymentsJob. -func (mr *MockDatastoreMockRecorder) RunNextBatchPaymentsJob(ctx, worker interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunNextBatchPaymentsJob", reflect.TypeOf((*MockDatastore)(nil).RunNextBatchPaymentsJob), ctx, worker) -} - // RunNextClaimJob mocks base method. func (m *MockDatastore) RunNextClaimJob(ctx context.Context, worker ClaimWorker) (bool, error) { m.ctrl.T.Helper() @@ -609,65 +501,6 @@ func (mr *MockDatastoreMockRecorder) RunNextClaimJob(ctx, worker interface{}) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunNextClaimJob", reflect.TypeOf((*MockDatastore)(nil).RunNextClaimJob), ctx, worker) } -// RunNextDrainJob mocks base method. -func (m *MockDatastore) RunNextDrainJob(ctx context.Context, worker DrainWorker) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RunNextDrainJob", ctx, worker) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// RunNextDrainJob indicates an expected call of RunNextDrainJob. -func (mr *MockDatastoreMockRecorder) RunNextDrainJob(ctx, worker interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunNextDrainJob", reflect.TypeOf((*MockDatastore)(nil).RunNextDrainJob), ctx, worker) -} - -// RunNextDrainRetryJob mocks base method. -func (m *MockDatastore) RunNextDrainRetryJob(ctx context.Context, worker DrainRetryWorker) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RunNextDrainRetryJob", ctx, worker) - ret0, _ := ret[0].(error) - return ret0 -} - -// RunNextDrainRetryJob indicates an expected call of RunNextDrainRetryJob. -func (mr *MockDatastoreMockRecorder) RunNextDrainRetryJob(ctx, worker interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunNextDrainRetryJob", reflect.TypeOf((*MockDatastore)(nil).RunNextDrainRetryJob), ctx, worker) -} - -// RunNextGeminiCheckStatus mocks base method. -func (m *MockDatastore) RunNextGeminiCheckStatus(ctx context.Context, worker GeminiTxnStatusWorker) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RunNextGeminiCheckStatus", ctx, worker) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// RunNextGeminiCheckStatus indicates an expected call of RunNextGeminiCheckStatus. -func (mr *MockDatastoreMockRecorder) RunNextGeminiCheckStatus(ctx, worker interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunNextGeminiCheckStatus", reflect.TypeOf((*MockDatastore)(nil).RunNextGeminiCheckStatus), ctx, worker) -} - -// RunNextMintDrainJob mocks base method. -func (m *MockDatastore) RunNextMintDrainJob(ctx context.Context, worker MintWorker) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RunNextMintDrainJob", ctx, worker) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// RunNextMintDrainJob indicates an expected call of RunNextMintDrainJob. -func (mr *MockDatastoreMockRecorder) RunNextMintDrainJob(ctx, worker interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunNextMintDrainJob", reflect.TypeOf((*MockDatastore)(nil).RunNextMintDrainJob), ctx, worker) -} - // RunNextSuggestionJob mocks base method. func (m *MockDatastore) RunNextSuggestionJob(ctx context.Context, worker SuggestionWorker) (bool, error) { m.ctrl.T.Helper() @@ -697,34 +530,6 @@ func (mr *MockDatastoreMockRecorder) SaveClaimCreds(claimCreds interface{}) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveClaimCreds", reflect.TypeOf((*MockDatastore)(nil).SaveClaimCreds), claimCreds) } -// SetMintDrainPromotionTotal mocks base method. -func (m *MockDatastore) SetMintDrainPromotionTotal(ctx context.Context, walletID, promotionID go_uuid.UUID, total decimal.Decimal) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetMintDrainPromotionTotal", ctx, walletID, promotionID, total) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetMintDrainPromotionTotal indicates an expected call of SetMintDrainPromotionTotal. -func (mr *MockDatastoreMockRecorder) SetMintDrainPromotionTotal(ctx, walletID, promotionID, total interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMintDrainPromotionTotal", reflect.TypeOf((*MockDatastore)(nil).SetMintDrainPromotionTotal), ctx, walletID, promotionID, total) -} - -// UpdateDrainJobAsRetriable mocks base method. -func (m *MockDatastore) UpdateDrainJobAsRetriable(ctx context.Context, walletID go_uuid.UUID) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateDrainJobAsRetriable", ctx, walletID) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpdateDrainJobAsRetriable indicates an expected call of UpdateDrainJobAsRetriable. -func (mr *MockDatastoreMockRecorder) UpdateDrainJobAsRetriable(ctx, walletID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDrainJobAsRetriable", reflect.TypeOf((*MockDatastore)(nil).UpdateDrainJobAsRetriable), ctx, walletID) -} - // UpdateOrder mocks base method. func (m *MockDatastore) UpdateOrder(orderID go_uuid.UUID, status string) error { m.ctrl.T.Helper() @@ -852,51 +657,6 @@ func (mr *MockReadOnlyDatastoreMockRecorder) GetClaimSummary(walletID, grantType return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClaimSummary", reflect.TypeOf((*MockReadOnlyDatastore)(nil).GetClaimSummary), walletID, grantType) } -// GetCustodianDrainInfo mocks base method. -func (m *MockReadOnlyDatastore) GetCustodianDrainInfo(paymentID *go_uuid.UUID) ([]CustodianDrain, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCustodianDrainInfo", paymentID) - ret0, _ := ret[0].([]CustodianDrain) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetCustodianDrainInfo indicates an expected call of GetCustodianDrainInfo. -func (mr *MockReadOnlyDatastoreMockRecorder) GetCustodianDrainInfo(paymentID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCustodianDrainInfo", reflect.TypeOf((*MockReadOnlyDatastore)(nil).GetCustodianDrainInfo), paymentID) -} - -// GetDrainPoll mocks base method. -func (m *MockReadOnlyDatastore) GetDrainPoll(drainID *go_uuid.UUID) (*DrainPoll, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDrainPoll", drainID) - ret0, _ := ret[0].(*DrainPoll) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetDrainPoll indicates an expected call of GetDrainPoll. -func (mr *MockReadOnlyDatastoreMockRecorder) GetDrainPoll(drainID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDrainPoll", reflect.TypeOf((*MockReadOnlyDatastore)(nil).GetDrainPoll), drainID) -} - -// GetDrainsByBatchID mocks base method. -func (m *MockReadOnlyDatastore) GetDrainsByBatchID(ctx context.Context, batchID *go_uuid.UUID) ([]DrainTransfer, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDrainsByBatchID", ctx, batchID) - ret0, _ := ret[0].([]DrainTransfer) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetDrainsByBatchID indicates an expected call of GetDrainsByBatchID. -func (mr *MockReadOnlyDatastoreMockRecorder) GetDrainsByBatchID(ctx, batchID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDrainsByBatchID", reflect.TypeOf((*MockReadOnlyDatastore)(nil).GetDrainsByBatchID), ctx, batchID) -} - // GetIssuer mocks base method. func (m *MockReadOnlyDatastore) GetIssuer(promotionID go_uuid.UUID, cohort string) (*Issuer, error) { m.ctrl.T.Helper() diff --git a/services/promotion/mockdrain.go b/services/promotion/mockdrain.go index 4440ad3ad..156c80ce0 100644 --- a/services/promotion/mockdrain.go +++ b/services/promotion/mockdrain.go @@ -3,207 +3,3 @@ // Package promotion is a generated GoMock package. package promotion - -import ( - context "context" - reflect "reflect" - - cbr "github.com/brave-intl/bat-go/libs/clients/cbr" - wallet "github.com/brave-intl/bat-go/libs/wallet" - gomock "github.com/golang/mock/gomock" - go_uuid "github.com/satori/go.uuid" - decimal "github.com/shopspring/decimal" -) - -// MockDrainWorker is a mock of DrainWorker interface. -type MockDrainWorker struct { - ctrl *gomock.Controller - recorder *MockDrainWorkerMockRecorder -} - -// MockDrainWorkerMockRecorder is the mock recorder for MockDrainWorker. -type MockDrainWorkerMockRecorder struct { - mock *MockDrainWorker -} - -// NewMockDrainWorker creates a new mock instance. -func NewMockDrainWorker(ctrl *gomock.Controller) *MockDrainWorker { - mock := &MockDrainWorker{ctrl: ctrl} - mock.recorder = &MockDrainWorkerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockDrainWorker) EXPECT() *MockDrainWorkerMockRecorder { - return m.recorder -} - -// RedeemAndTransferFunds mocks base method. -func (m *MockDrainWorker) RedeemAndTransferFunds(ctx context.Context, credentials []cbr.CredentialRedemption, drainJob DrainJob) (*wallet.TransactionInfo, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RedeemAndTransferFunds", ctx, credentials, drainJob) - ret0, _ := ret[0].(*wallet.TransactionInfo) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// RedeemAndTransferFunds indicates an expected call of RedeemAndTransferFunds. -func (mr *MockDrainWorkerMockRecorder) RedeemAndTransferFunds(ctx, credentials, drainJob interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RedeemAndTransferFunds", reflect.TypeOf((*MockDrainWorker)(nil).RedeemAndTransferFunds), ctx, credentials, drainJob) -} - -// MockDrainRetryWorker is a mock of DrainRetryWorker interface. -type MockDrainRetryWorker struct { - ctrl *gomock.Controller - recorder *MockDrainRetryWorkerMockRecorder -} - -// MockDrainRetryWorkerMockRecorder is the mock recorder for MockDrainRetryWorker. -type MockDrainRetryWorkerMockRecorder struct { - mock *MockDrainRetryWorker -} - -// NewMockDrainRetryWorker creates a new mock instance. -func NewMockDrainRetryWorker(ctrl *gomock.Controller) *MockDrainRetryWorker { - mock := &MockDrainRetryWorker{ctrl: ctrl} - mock.recorder = &MockDrainRetryWorkerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockDrainRetryWorker) EXPECT() *MockDrainRetryWorkerMockRecorder { - return m.recorder -} - -// FetchAdminAttestationWalletID mocks base method. -func (m *MockDrainRetryWorker) FetchAdminAttestationWalletID(ctx context.Context) (*go_uuid.UUID, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FetchAdminAttestationWalletID", ctx) - ret0, _ := ret[0].(*go_uuid.UUID) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FetchAdminAttestationWalletID indicates an expected call of FetchAdminAttestationWalletID. -func (mr *MockDrainRetryWorkerMockRecorder) FetchAdminAttestationWalletID(ctx interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchAdminAttestationWalletID", reflect.TypeOf((*MockDrainRetryWorker)(nil).FetchAdminAttestationWalletID), ctx) -} - -// MockMintWorker is a mock of MintWorker interface. -type MockMintWorker struct { - ctrl *gomock.Controller - recorder *MockMintWorkerMockRecorder -} - -// MockMintWorkerMockRecorder is the mock recorder for MockMintWorker. -type MockMintWorkerMockRecorder struct { - mock *MockMintWorker -} - -// NewMockMintWorker creates a new mock instance. -func NewMockMintWorker(ctrl *gomock.Controller) *MockMintWorker { - mock := &MockMintWorker{ctrl: ctrl} - mock.recorder = &MockMintWorkerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockMintWorker) EXPECT() *MockMintWorkerMockRecorder { - return m.recorder -} - -// MintGrant mocks base method. -func (m *MockMintWorker) MintGrant(ctx context.Context, walletID go_uuid.UUID, total decimal.Decimal, promoIDs ...go_uuid.UUID) error { - m.ctrl.T.Helper() - varargs := []interface{}{ctx, walletID, total} - for _, a := range promoIDs { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "MintGrant", varargs...) - ret0, _ := ret[0].(error) - return ret0 -} - -// MintGrant indicates an expected call of MintGrant. -func (mr *MockMintWorkerMockRecorder) MintGrant(ctx, walletID, total interface{}, promoIDs ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{ctx, walletID, total}, promoIDs...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MintGrant", reflect.TypeOf((*MockMintWorker)(nil).MintGrant), varargs...) -} - -// MockBatchTransferWorker is a mock of BatchTransferWorker interface. -type MockBatchTransferWorker struct { - ctrl *gomock.Controller - recorder *MockBatchTransferWorkerMockRecorder -} - -// MockBatchTransferWorkerMockRecorder is the mock recorder for MockBatchTransferWorker. -type MockBatchTransferWorkerMockRecorder struct { - mock *MockBatchTransferWorker -} - -// NewMockBatchTransferWorker creates a new mock instance. -func NewMockBatchTransferWorker(ctrl *gomock.Controller) *MockBatchTransferWorker { - mock := &MockBatchTransferWorker{ctrl: ctrl} - mock.recorder = &MockBatchTransferWorkerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockBatchTransferWorker) EXPECT() *MockBatchTransferWorkerMockRecorder { - return m.recorder -} - -// SubmitBatchTransfer mocks base method. -func (m *MockBatchTransferWorker) SubmitBatchTransfer(ctx context.Context, batchID *go_uuid.UUID) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SubmitBatchTransfer", ctx, batchID) - ret0, _ := ret[0].(error) - return ret0 -} - -// SubmitBatchTransfer indicates an expected call of SubmitBatchTransfer. -func (mr *MockBatchTransferWorkerMockRecorder) SubmitBatchTransfer(ctx, batchID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitBatchTransfer", reflect.TypeOf((*MockBatchTransferWorker)(nil).SubmitBatchTransfer), ctx, batchID) -} - -// MockGeminiTxnStatusWorker is a mock of GeminiTxnStatusWorker interface. -type MockGeminiTxnStatusWorker struct { - ctrl *gomock.Controller - recorder *MockGeminiTxnStatusWorkerMockRecorder -} - -// MockGeminiTxnStatusWorkerMockRecorder is the mock recorder for MockGeminiTxnStatusWorker. -type MockGeminiTxnStatusWorkerMockRecorder struct { - mock *MockGeminiTxnStatusWorker -} - -// NewMockGeminiTxnStatusWorker creates a new mock instance. -func NewMockGeminiTxnStatusWorker(ctrl *gomock.Controller) *MockGeminiTxnStatusWorker { - mock := &MockGeminiTxnStatusWorker{ctrl: ctrl} - mock.recorder = &MockGeminiTxnStatusWorkerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockGeminiTxnStatusWorker) EXPECT() *MockGeminiTxnStatusWorkerMockRecorder { - return m.recorder -} - -// GetGeminiTxnStatus mocks base method. -func (m *MockGeminiTxnStatusWorker) GetGeminiTxnStatus(ctx context.Context, txRef string) (*wallet.TransactionInfo, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGeminiTxnStatus", ctx, txRef) - ret0, _ := ret[0].(*wallet.TransactionInfo) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetGeminiTxnStatus indicates an expected call of GetGeminiTxnStatus. -func (mr *MockGeminiTxnStatusWorkerMockRecorder) GetGeminiTxnStatus(ctx, txRef interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGeminiTxnStatus", reflect.TypeOf((*MockGeminiTxnStatusWorker)(nil).GetGeminiTxnStatus), ctx, txRef) -} diff --git a/services/promotion/service.go b/services/promotion/service.go index c84d1e8c1..f69213b9b 100644 --- a/services/promotion/service.go +++ b/services/promotion/service.go @@ -6,14 +6,11 @@ import ( "errors" "fmt" "os" - "strconv" "sync" "time" "github.com/brave-intl/bat-go/libs/altcurrency" - "github.com/brave-intl/bat-go/libs/clients/bitflyer" "github.com/brave-intl/bat-go/libs/clients/cbr" - "github.com/brave-intl/bat-go/libs/clients/gemini" "github.com/brave-intl/bat-go/libs/clients/reputation" appctx "github.com/brave-intl/bat-go/libs/context" errorutils "github.com/brave-intl/bat-go/libs/errors" @@ -33,11 +30,6 @@ import ( const localEnv = "local" var ( - // toggle for drain retry job - enableDrainRetryJob = isDrainRetryJobEnabled() - // toggle for gemini check status - enableGemini = isGeminiEnabled() - suggestionTopic = os.Getenv("ENV") + ".grant.suggestion" adminAttestationTopic = fmt.Sprintf("admin_attestation_events.%s.repsys.upstream", os.Getenv("ENV")) @@ -78,30 +70,6 @@ var ( ) ) -func isDrainRetryJobEnabled() bool { - var toggle = false - if os.Getenv("DRAIN_RETRY_JOB_ENABLED") != "" { - var err error - toggle, err = strconv.ParseBool(os.Getenv("DRAIN_RETRY_JOB_ENABLED")) - if err != nil { - return false - } - } - return toggle -} - -func isGeminiEnabled() bool { - var toggle = false - if os.Getenv("GEMINI_ENABLED") != "" { - var err error - toggle, err = strconv.ParseBool(os.Getenv("GEMINI_ENABLED")) - if err != nil { - return false - } - } - return toggle -} - // SetSuggestionTopic allows for a new topic to be suggested func SetSuggestionTopic(newTopic string) { suggestionTopic = newTopic @@ -119,15 +87,11 @@ type Service struct { RoDatastore ReadOnlyDatastore cbClient cbr.Client reputationClient reputation.Client - bfClient bitflyer.Client - geminiClient gemini.Client - geminiConf *gemini.Conf codecs map[string]*goavro.Codec kafkaWriter *kafka.Writer kafkaDialer *kafka.Dialer kafkaAdminAttestationReader kafkautils.Consumer hotWallet *uphold.Wallet - drainChannel chan *w.TransactionInfo jobs []srv.Job pauseSuggestionsUntil time.Time pauseSuggestionsUntilMu sync.RWMutex @@ -150,20 +114,6 @@ func (service *Service) InitKafka(ctx context.Context) error { return fmt.Errorf("failed to initialize kafka: %w", err) } - // toggle for drain retry job - if enableDrainRetryJob { - groupID := os.Getenv("KAFKA_CONSUMER_GROUP_PROMOTIONS") - if groupID == "" { - return errors.New("failed not initialize kafka could not find consumer group") - } - - reader, err := kafkautils.NewKafkaReader(ctx, groupID, adminAttestationTopic) - if err != nil { - return fmt.Errorf("failed to initialize kafka attestation reader: %w", err) - } - service.kafkaAdminAttestationReader = reader - } - service.codecs, err = kafkautils.GenerateCodecs(map[string]string{ "suggestion": suggestionEventSchema, adminAttestationTopic: adminAttestationEventSchema, @@ -221,9 +171,6 @@ func InitService( walletService *wallet.Service, ) (*Service, error) { - // get logger from context - logger := logging.Logger(ctx, "promotion.InitService") - // register metrics with prometheus if err := prometheus.Register(countGrantsClaimedBatTotal); err != nil { if ae, ok := err.(prometheus.AlreadyRegisteredError); ok { @@ -253,41 +200,6 @@ func InitService( return nil, err } - var bfClient bitflyer.Client - // setup bfClient - if os.Getenv("BITFLYER_ENABLED") == "true" { - bfClient, err = bitflyer.New() - if err != nil { - return nil, fmt.Errorf("failed to create bitflyer client: %w", err) - } - // get a fresh bf token - // this will set the AuthToken on the client for us - _, err = bfClient.RefreshToken(ctx, bitflyer.TokenPayloadFromCtx(ctx)) - if err != nil { - // we don't want to stop the world if we can't connect to bf - logger.Error().Err(err).Msg("failed to get bf access token!") - } - } - - var ( - geminiClient gemini.Client - geminiConf *gemini.Conf - ) - if os.Getenv("GEMINI_ENABLED") == "true" { - // get the correct env variables for bulk pay API call - geminiConf = &gemini.Conf{ - ClientID: os.Getenv("GEMINI_CLIENT_ID"), - APIKey: os.Getenv("GEMINI_API_KEY"), - Secret: os.Getenv("GEMINI_API_SECRET"), - } - - gc, err := gemini.New() - if err != nil { - return nil, fmt.Errorf("failed to create gemini client: %w", err) - } - geminiClient = gc - } - reputationClient, err := reputation.New() // okay to fail to make a reputation client if the environment is local if err != nil && os.Getenv("ENV") != localEnv { @@ -298,9 +210,6 @@ func InitService( Datastore: promotionDB, RoDatastore: promotionRODB, cbClient: cbClient, - bfClient: bfClient, - geminiClient: geminiClient, - geminiConf: geminiConf, reputationClient: reputationClient, wallet: walletService, pauseSuggestionsUntilMu: sync.RWMutex{}, @@ -323,55 +232,6 @@ func InitService( Cadence: 5 * time.Second, Workers: 1, }, - { - Func: service.RunNextMintDrainJob, - Cadence: time.Second, - Workers: 6, - }, - { - Func: service.RunNextBatchPaymentsJob, - Cadence: time.Second, - Workers: 1, - }, - } - - // toggle for drain retry job - if enableDrainRetryJob { - service.jobs = append(service.jobs, - srv.Job{ - Func: service.RunNextDrainRetryJob, - Cadence: 5 * time.Second, - Workers: 1, - }) - } - - // toggle for gemini check status - if enableGemini { - service.jobs = append(service.jobs, - srv.Job{ - Func: service.RunNextGeminiCheckStatus, - Cadence: time.Second, - Workers: 1, - }) - } - - var enableLinkingDraining bool - // make sure that we only enable the DrainJob if we have linking/draining enabled - if os.Getenv("ENABLE_LINKING_DRAINING") != "" { - enableLinkingDraining, err = strconv.ParseBool(os.Getenv("ENABLE_LINKING_DRAINING")) - if err != nil { - // there was an error parsing the environment variable - return nil, fmt.Errorf("invalid enable_linking_draining flag: %w", err) - } - } - - if enableLinkingDraining { - service.jobs = append(service.jobs, - srv.Job{ - Func: service.RunNextDrainJob, - Cadence: 5 * time.Second, - Workers: 1, - }) } err = service.InitKafka(ctx) @@ -394,16 +254,6 @@ func (service *Service) ReadableDatastore() ReadOnlyDatastore { return service.Datastore } -// RunNextMintDrainJob takes the next mint job and completes it -func (service *Service) RunNextMintDrainJob(ctx context.Context) (bool, error) { - return service.Datastore.RunNextMintDrainJob(ctx, service) -} - -// RunNextBatchPaymentsJob takes the next claim job and completes it -func (service *Service) RunNextBatchPaymentsJob(ctx context.Context) (bool, error) { - return service.Datastore.RunNextBatchPaymentsJob(ctx, service) -} - // RunNextClaimJob takes the next claim job and completes it func (service *Service) RunNextClaimJob(ctx context.Context) (bool, error) { return service.Datastore.RunNextClaimJob(ctx, service) @@ -414,16 +264,6 @@ func (service *Service) RunNextSuggestionJob(ctx context.Context) (bool, error) return service.Datastore.RunNextSuggestionJob(ctx, service) } -// RunNextDrainJob takes the next drain job and completes it -func (service *Service) RunNextDrainJob(ctx context.Context) (bool, error) { - return service.Datastore.RunNextDrainJob(ctx, service) -} - -// RunNextDrainRetryJob - retires failed drain jobs -func (service *Service) RunNextDrainRetryJob(ctx context.Context) (bool, error) { - return true, service.Datastore.RunNextDrainRetryJob(ctx, service) -} - // RunNextPromotionMissingIssuer takes the next job and completes it func (service *Service) RunNextPromotionMissingIssuer(ctx context.Context) (bool, error) { // get logger from context @@ -443,8 +283,3 @@ func (service *Service) RunNextPromotionMissingIssuer(ctx context.Context) (bool } return true, nil } - -// RunNextGeminiCheckStatus periodically check the status of gemini claim drain transactions -func (service *Service) RunNextGeminiCheckStatus(ctx context.Context) (bool, error) { - return service.Datastore.RunNextGeminiCheckStatus(ctx, service) -} diff --git a/services/promotion/service_test.go b/services/promotion/service_test.go index 6521feb5c..04d45052b 100644 --- a/services/promotion/service_test.go +++ b/services/promotion/service_test.go @@ -4,21 +4,12 @@ package promotion import ( "context" - "encoding/json" - "fmt" - "math/rand" - "os" - "strings" "testing" - "time" appctx "github.com/brave-intl/bat-go/libs/context" "github.com/brave-intl/bat-go/libs/inputs" - kafkautils "github.com/brave-intl/bat-go/libs/kafka" "github.com/brave-intl/bat-go/libs/test" "github.com/brave-intl/bat-go/services/wallet" - uuid "github.com/satori/go.uuid" - "github.com/segmentio/kafka-go" // re-using viper bind-env for wallet env variables _ "github.com/brave-intl/bat-go/services/wallet/cmd" @@ -114,104 +105,3 @@ func (suite *ServiceTestSuite) TestGetAvailablePromotions() { suite.Require().NoError(err) suite.Require().Equal(nilPromotions, promotions) } - -func (suite *ServiceTestSuite) TestInitAndRunNextDrainRetryJob() { - // seed the failed drain jobs - pg, _, err := NewPostgres() - suite.Require().NoError(err) - - query := `INSERT INTO claim_drain (wallet_id, erred, errcode, status, batch_id, credentials, completed, total) - VALUES ($1, $2, $3, $4, $5, '[{"t":"123"}]', FALSE, 1);` - - walletIDs := make([]uuid.UUID, 5, 5) - for i := 0; i < 5; i++ { - walletIDs[i] = uuid.NewV4() - - _, err = pg.RawDB().ExecContext(context.Background(), query, walletIDs[i].String(), true, - "reputation-failed", "reputation-failed", uuid.NewV4().String()) - - suite.Require().NoError(err, "should have inserted claim drain row") - } - - // setup kafka topic and dialer - SetAdminAttestationTopic(fmt.Sprintf("admin_attestation_events.%s.repsys.upstream", uuid.NewV4().String())) - - kafkaBrokers := os.Getenv("KAFKA_BROKERS") - c := context.WithValue(context.Background(), appctx.KafkaBrokersCTXKey, kafkaBrokers) - ctx, cancel := context.WithCancel(c) - defer cancel() - - dialer, _, err := kafkautils.TLSDialer() - suite.Require().NoError(err) - conn, err := dialer.DialLeader(ctx, "tcp", strings.Split(kafkaBrokers, ",")[0], adminAttestationTopic, 0) - suite.Require().NoError(err) - - err = conn.CreateTopics(kafka.TopicConfig{Topic: adminAttestationTopic, NumPartitions: 1, ReplicationFactor: 1}) - suite.Require().NoError(err) - - kafkaWriter, _, err := kafkautils.InitKafkaWriter(ctx, adminAttestationTopic) - suite.Require().NoError(err) - - codecs, err := kafkautils.GenerateCodecs(map[string]string{ - adminAttestationTopic: adminAttestationEventSchema, - }) - - randomString := func() string { - return uuid.NewV4().String() - } - - msgCount := 5 - for i := 0; i < msgCount; i++ { - msg := AdminAttestationEvent{ - WalletID: walletIDs[i].String(), - Service: randomString(), - Signal: randomString(), - Score: rand.Int31n(10), - Justification: randomString(), - CreatedAt: time.Now().String(), - } - - textual, err := json.Marshal(msg) - suite.Require().NoError(err) - - native, _, err := codecs[adminAttestationTopic].NativeFromTextual(textual) - suite.Require().NoError(err) - - binary, err := codecs[adminAttestationTopic].BinaryFromNative(nil, native) - suite.Require().NoError(err) - - err = kafkaWriter.WriteMessages(ctx, kafka.Message{ - Key: []byte(walletIDs[i].String()), - Value: binary, - }) - suite.Require().NoError(err) - } - - // start service - go func(ctx context.Context) { - service, _ := InitService(ctx, pg, nil, nil) - service.RunNextDrainRetryJob(ctx) - }(ctx) - - // assert drain job has been updated - index := 0 - end := time.Now().Add(60 * time.Second) // max timeout - var drainJob DrainJob - for { - if time.Now().After(end) { - suite.Require().Fail("test failed due to timeout") - } - if index >= msgCount { - break - } - // select the drain job and if erred is false check it has been fully updated - err = pg.RawDB().Get(&drainJob, `SELECT * FROM claim_drain WHERE wallet_id = $1 LIMIT 1`, walletIDs[index]) - if drainJob.Erred == false { - suite.Require().Equal(walletIDs[index], drainJob.WalletID) - suite.Require().Equal(false, drainJob.Erred) - suite.Require().Equal("reputation-failed", *drainJob.ErrCode) - suite.Require().Equal("retry-bypass-cbr", *drainJob.Status) - index += 1 - } - } -} diff --git a/services/rewards/docker-compose.yml b/services/rewards/docker-compose.yml index c2f0b3a03..522521a8f 100644 --- a/services/rewards/docker-compose.yml +++ b/services/rewards/docker-compose.yml @@ -39,7 +39,7 @@ services: rewards-dev-refresh: container_name: rewards-dev-refresh - image: golang:1.18 + image: golang:1.19 ports: - "3343:3343" - "6061:6061" diff --git a/services/skus/controllers.go b/services/skus/controllers.go index 1b6ce38c7..2ab672544 100644 --- a/services/skus/controllers.go +++ b/services/skus/controllers.go @@ -330,11 +330,13 @@ func CancelOrder(service *Service) handlers.AppHandler { ) } - if err := service.validateOrderMerchantAndCaveats(ctx, *orderID.UUID()); err != nil { + oid := *orderID.UUID() + + if err := service.validateOrderMerchantAndCaveats(ctx, oid); err != nil { return handlers.WrapError(err, "Error validating auth merchant and caveats", http.StatusForbidden) } - if err := service.CancelOrder(*orderID.UUID()); err != nil { + if err := service.CancelOrder(oid); err != nil { return handlers.WrapError(err, "Error retrieving the order", http.StatusInternalServerError) } @@ -1064,151 +1066,124 @@ func HandleRadomWebhook(service *Service) handlers.AppHandler { } } -// HandleStripeWebhook is the handler for stripe checkout session webhooks +// HandleStripeWebhook handles webhook events from Stripe. func HandleStripeWebhook(service *Service) handlers.AppHandler { return func(w http.ResponseWriter, r *http.Request) *handlers.AppError { ctx := r.Context() - // get logger - sublogger := logging.Logger(ctx, "payments").With(). - Str("func", "HandleStripeWebhook"). - Logger() - // get webhook secret from ctx + lg := logging.Logger(ctx, "payments").With().Str("func", "HandleStripeWebhook").Logger() + endpointSecret, err := appctx.GetStringFromContext(ctx, appctx.StripeWebhookSecretCTXKey) if err != nil { - sublogger.Error().Err(err).Msg("failed to get stripe_webhook_secret from context") - return handlers.WrapError( - err, "error getting stripe_webhook_secret from context", - http.StatusInternalServerError) + lg.Error().Err(err).Msg("failed to get stripe_webhook_secret from context") + return handlers.WrapError(err, "error getting stripe_webhook_secret from context", http.StatusInternalServerError) } - b, err := requestutils.Read(r.Context(), r.Body) + b, err := requestutils.Read(ctx, r.Body) if err != nil { - sublogger.Error().Err(err).Msg("failed to read request body") + lg.Error().Err(err).Msg("failed to read request body") return handlers.WrapError(err, "error reading request body", http.StatusServiceUnavailable) } - event, err := webhook.ConstructEvent( - b, r.Header.Get("Stripe-Signature"), endpointSecret) + event, err := webhook.ConstructEvent(b, r.Header.Get("Stripe-Signature"), endpointSecret) if err != nil { - sublogger.Error().Err(err).Msg("failed to verify stripe signature") + lg.Error().Err(err).Msg("failed to verify stripe signature") return handlers.WrapError(err, "error verifying webhook signature", http.StatusBadRequest) } - // log the event - sublogger.Debug().Str("event_type", event.Type).Str("data", string(event.Data.Raw)).Msg("webhook event captured") + switch event.Type { + case StripeInvoiceUpdated, StripeInvoicePaid: + // Handle invoice events. - // Handle invoice events - if event.Type == StripeInvoiceUpdated || event.Type == StripeInvoicePaid { - // Retrieve invoice from update events var invoice stripe.Invoice - err := json.Unmarshal(event.Data.Raw, &invoice) - if err != nil { - sublogger.Error().Err(err).Msg("error parsing webhook json") + if err := json.Unmarshal(event.Data.Raw, &invoice); err != nil { + lg.Error().Err(err).Msg("error parsing webhook json") return handlers.WrapError(err, "error parsing webhook JSON", http.StatusBadRequest) } - sublogger.Debug(). - Str("event_type", event.Type). - Str("invoice", fmt.Sprintf("%+v", invoice)).Msg("webhook invoice") subscription, err := service.scClient.Subscriptions.Get(invoice.Subscription.ID, nil) if err != nil { - sublogger.Error().Err(err).Msg("error getting subscription") + lg.Error().Err(err).Msg("error getting subscription") return handlers.WrapError(err, "error retrieving subscription", http.StatusInternalServerError) } - sublogger.Debug(). - Str("subscription", fmt.Sprintf("%+v", subscription)).Msg("corresponding subscription") - orderID, err := uuid.FromString(subscription.Metadata["orderID"]) if err != nil { - sublogger.Error().Err(err).Msg("error getting order id from subscription metadata") + lg.Error().Err(err).Msg("error getting order id from subscription metadata") return handlers.WrapError(err, "error retrieving orderID", http.StatusInternalServerError) } - sublogger.Debug(). - Str("orderID", orderID.String()).Msg("order id") - // If the invoice is paid set order status to paid, otherwise if invoice.Paid { - // is this an existing subscription?? ok, subID, err := service.Datastore.IsStripeSub(orderID) if err != nil { - sublogger.Error().Err(err).Msg("failed to tell if this is a stripe subscription") + lg.Error().Err(err).Msg("failed to tell if this is a stripe subscription") return handlers.WrapError(err, "error looking up payment provider", http.StatusInternalServerError) } + + // Handle renewal. if ok && subID != "" { - // okay, this is a subscription renewal, not first time, - err = service.RenewOrder(ctx, orderID) - if err != nil { - sublogger.Error().Err(err).Msg("failed to renew the order") + if err := service.RenewOrder(ctx, orderID); err != nil { + lg.Error().Err(err).Msg("failed to renew the order") return handlers.WrapError(err, "error renewing order", http.StatusInternalServerError) } - // end flow for renew order - return handlers.RenderContent(r.Context(), "subscription renewed", w, http.StatusOK) + + return handlers.RenderContent(ctx, "subscription renewed", w, http.StatusOK) } - // not a renewal, first time - // and update the order's expires at as it was just paid - err = service.Datastore.UpdateOrder(orderID, OrderStatusPaid) - if err != nil { - sublogger.Error().Err(err).Msg("failed to update order status") + // New subscription. + // Update the order's expires at as it was just paid. + if err := service.Datastore.UpdateOrder(orderID, OrderStatusPaid); err != nil { + lg.Error().Err(err).Msg("failed to update order status") return handlers.WrapError(err, "error updating order status", http.StatusInternalServerError) } - err = service.Datastore.AppendOrderMetadata(ctx, &orderID, "stripeSubscriptionId", subscription.ID) - if err != nil { - sublogger.Error().Err(err).Msg("failed to update order metadata") + + if err := service.Datastore.AppendOrderMetadata(ctx, &orderID, "stripeSubscriptionId", subscription.ID); err != nil { + lg.Error().Err(err).Msg("failed to update order metadata") return handlers.WrapError(err, "error updating order metadata", http.StatusInternalServerError) } - // set paymentProcessor as stripe - err = service.Datastore.AppendOrderMetadata(ctx, &orderID, paymentProcessor, StripePaymentMethod) - if err != nil { - sublogger.Error().Err(err).Msg("failed to update order to add the payment processor") + + if err := service.Datastore.AppendOrderMetadata(ctx, &orderID, paymentProcessor, StripePaymentMethod); err != nil { + lg.Error().Err(err).Msg("failed to update order to add the payment processor") return handlers.WrapError(err, "failed to update order to add the payment processor", http.StatusInternalServerError) } - sublogger.Debug().Str("orderID", orderID.String()).Msg("order is now paid") - return handlers.RenderContent(r.Context(), "payment successful", w, http.StatusOK) + return handlers.RenderContent(ctx, "payment successful", w, http.StatusOK) } - sublogger.Debug(). - Str("orderID", orderID.String()).Msg("order not paid, set pending") - err = service.Datastore.UpdateOrder(orderID, "pending") - if err != nil { - sublogger.Error().Err(err).Msg("failed to update order status") + if err := service.Datastore.UpdateOrder(orderID, "pending"); err != nil { + lg.Error().Err(err).Msg("failed to update order status") return handlers.WrapError(err, "error updating order status", http.StatusInternalServerError) } - sublogger.Debug(). - Str("sub_id", subscription.ID).Msg("set subscription id in order metadata") - err = service.Datastore.AppendOrderMetadata(ctx, &orderID, "stripeSubscriptionId", subscription.ID) - if err != nil { - sublogger.Error().Err(err).Msg("failed to update order metadata") + + if err := service.Datastore.AppendOrderMetadata(ctx, &orderID, "stripeSubscriptionId", subscription.ID); err != nil { + lg.Error().Err(err).Msg("failed to update order metadata") return handlers.WrapError(err, "error updating order metadata", http.StatusInternalServerError) } - sublogger.Debug(). - Str("sub_id", subscription.ID).Msg("set ok response") - return handlers.RenderContent(r.Context(), "payment failed", w, http.StatusOK) - } - // Handle subscription cancellations - if event.Type == StripeCustomerSubscriptionDeleted { + return handlers.RenderContent(ctx, "payment failed", w, http.StatusOK) + + case StripeCustomerSubscriptionDeleted: + // Handle subscription cancellations + var subscription stripe.Subscription - err := json.Unmarshal(event.Data.Raw, &subscription) - if err != nil { + if err := json.Unmarshal(event.Data.Raw, &subscription); err != nil { return handlers.WrapError(err, "error parsing webhook JSON", http.StatusBadRequest) } + orderID, err := uuid.FromString(subscription.Metadata["orderID"]) if err != nil { return handlers.WrapError(err, "error retrieving orderID", http.StatusInternalServerError) } - err = service.Datastore.UpdateOrder(orderID, OrderStatusCanceled) - if err != nil { + + if err := service.Datastore.UpdateOrder(orderID, OrderStatusCanceled); err != nil { return handlers.WrapError(err, "error updating order status", http.StatusInternalServerError) } - return handlers.RenderContent(r.Context(), "subscription canceled", w, http.StatusOK) + + return handlers.RenderContent(ctx, "subscription canceled", w, http.StatusOK) } - return handlers.RenderContent(r.Context(), "event received", w, http.StatusOK) + return handlers.RenderContent(ctx, "event received", w, http.StatusOK) } } diff --git a/services/skus/docker-compose.payment-refresh.yml b/services/skus/docker-compose.payment-refresh.yml index 098c1575d..c48fee9d5 100644 --- a/services/skus/docker-compose.payment-refresh.yml +++ b/services/skus/docker-compose.payment-refresh.yml @@ -8,7 +8,7 @@ services: # every time a file changes. payment-refresh: container_name: grant-payment-refresh - image: golang:1.18 + image: golang:1.19 ports: - "3335:3333" - "6061:6061" @@ -38,7 +38,6 @@ services: - "DATABASE_MIGRATIONS_URL=file:///src/migrations" - "DATABASE_URL=postgres://grants:password@postgres/grants?sslmode=disable" - ENCRYPTION_KEY=MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0 - - FEATURE_MERCHANT=true - FEATURE_PAYMENT=true - DONOR_WALLET_CARD_ID - DONOR_WALLET_PRIVATE_KEY diff --git a/services/skus/docker-compose.yml b/services/skus/docker-compose.yml index 827e634fc..c01ca11aa 100644 --- a/services/skus/docker-compose.yml +++ b/services/skus/docker-compose.yml @@ -12,7 +12,7 @@ services: # every time a file changes. skus-dev-refresh: container_name: skus-dev-refresh - image: golang:1.18 + image: golang:1.19 ports: - "3353:3353" - "6061:6061" @@ -35,7 +35,6 @@ services: - "DATABASE_MIGRATIONS_URL=file:///src/migrations" - "DATABASE_URL=postgres://grants:password@postgres/grants?sslmode=disable" - ENCRYPTION_KEY=MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0 - - FEATURE_MERCHANT=true - KAFKA_BROKERS=kafka:19092 - KAFKA_SSL_CA_LOCATION=/etc/kafka/secrets/snakeoil-ca-1.crt - KAFKA_SSL_CERTIFICATE_LOCATION=/etc/kafka/secrets/consumer-ca1-signed.pem diff --git a/services/skus/instrumented_datastore.go b/services/skus/instrumented_datastore.go index bc6a28657..6a4f5b893 100644 --- a/services/skus/instrumented_datastore.go +++ b/services/skus/instrumented_datastore.go @@ -1,9 +1,9 @@ +package skus + // Code generated by gowrap. DO NOT EDIT. // template: ../../.prom-gowrap.tmpl // gowrap: http://github.com/hexdigest/gowrap -package skus - //go:generate gowrap gen -p github.com/brave-intl/bat-go/services/skus -i Datastore -t ../../.prom-gowrap.tmpl -o instrumented_datastore.go -l "" import ( @@ -156,8 +156,18 @@ func (_d DatastoreWithPrometheus) CreateKey(merchant string, name string, encryp return _d.base.CreateKey(merchant, name, encryptedSecretKey, nonce) } -func (_d DatastoreWithPrometheus) CreateOrder(ctx context.Context, dbi sqlx.ExtContext, req *model.OrderNew, items []model.OrderItem) (op1 *model.Order, err error) { - return _d.base.CreateOrder(ctx, dbi, req, items) +// CreateOrder implements Datastore +func (_d DatastoreWithPrometheus) CreateOrder(ctx context.Context, dbi sqlx.ExtContext, oreq *model.OrderNew, items []model.OrderItem) (op1 *model.Order, err error) { + _since := time.Now() + defer func() { + result := "ok" + if err != nil { + result = "error" + } + + datastoreDurationSummaryVec.WithLabelValues(_d.instanceName, "CreateOrder", result).Observe(time.Since(_since).Seconds()) + }() + return _d.base.CreateOrder(ctx, dbi, oreq, items) } // CreateTransaction implements Datastore diff --git a/services/skus/service.go b/services/skus/service.go index 88f099a84..13774dce5 100644 --- a/services/skus/service.go +++ b/services/skus/service.go @@ -585,10 +585,9 @@ func (s *Service) CancelOrder(orderID uuid.UUID) error { // Try to find order in Stripe. params := &stripe.SubscriptionSearchParams{} - params.Query = *stripe.String(fmt.Sprintf( - "status:'active' AND metadata['orderID']:'%s'", - orderID.String(), // orderID is already checked as uuid - )) + params.Query = *stripe.String(fmt.Sprintf("status:'active' AND metadata['orderID']:'%s'", orderID.String())) + + ctx := context.TODO() iter := sub.Search(params) for iter.Next() { @@ -598,7 +597,8 @@ func (s *Service) CancelOrder(orderID uuid.UUID) error { if _, err := sub.Cancel(subscription.ID, nil); err != nil { return fmt.Errorf("failed to cancel stripe subscription: %w", err) } - if err := s.Datastore.AppendOrderMetadata(context.Background(), &orderID, "stripeSubscriptionId", subscription.ID); err != nil { + + if err := s.Datastore.AppendOrderMetadata(ctx, &orderID, "stripeSubscriptionId", subscription.ID); err != nil { return fmt.Errorf("failed to update order metadata with subscription id: %w", err) } } diff --git a/services/skus/service_test.go b/services/skus/service_test.go index 807cc1518..683203ba0 100644 --- a/services/skus/service_test.go +++ b/services/skus/service_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/brave-intl/bat-go/libs/ptr" "github.com/shopspring/decimal" should "github.com/stretchr/testify/assert" must "github.com/stretchr/testify/require" @@ -89,7 +90,7 @@ func TestCreateOrderItems(t *testing.T) { Items: []model.OrderItemRequestNew{ { CredentialValidDuration: "P1M", - CredentialValidDurationEach: ptrTo("rubbish"), + CredentialValidDurationEach: ptr.To("rubbish"), }, }, }, @@ -120,7 +121,7 @@ func TestCreateOrderItems(t *testing.T) { result: []model.OrderItem{ { Currency: "USD", - ValidForISO: ptrTo("P1M"), + ValidForISO: ptr.To("P1M"), Location: datastore.NullString{ NullString: sql.NullString{ Valid: true, @@ -142,7 +143,7 @@ func TestCreateOrderItems(t *testing.T) { { Currency: "USD", - ValidForISO: ptrTo("P1M"), + ValidForISO: ptr.To("P1M"), Location: datastore.NullString{ NullString: sql.NullString{ Valid: true, @@ -206,7 +207,7 @@ func TestCreateOrderItem(t *testing.T) { { name: "invalid_CredentialValidDurationEach", given: &model.OrderItemRequestNew{ - CredentialValidDurationEach: ptrTo("rubbish"), + CredentialValidDurationEach: ptr.To("rubbish"), }, exp: tcExpected{ err: timeutils.ErrUnsupportedFormat, @@ -217,7 +218,7 @@ func TestCreateOrderItem(t *testing.T) { name: "invalid_CredentialValidDuration", given: &model.OrderItemRequestNew{ CredentialValidDuration: "rubbish", - CredentialValidDurationEach: ptrTo("P1M"), + CredentialValidDurationEach: ptr.To("P1M"), }, exp: tcExpected{ err: timeutils.ErrUnsupportedFormat, @@ -230,8 +231,8 @@ func TestCreateOrderItem(t *testing.T) { SKU: "sku", CredentialType: "credential_type", CredentialValidDuration: "P1M", - CredentialValidDurationEach: ptrTo("P1D"), - IssuanceInterval: ptrTo("P1M"), + CredentialValidDurationEach: ptr.To("P1D"), + IssuanceInterval: ptr.To("P1M"), Price: decimal.NewFromInt(10), Location: "location", Description: "description", @@ -247,9 +248,9 @@ func TestCreateOrderItem(t *testing.T) { SKU: "sku", CredentialType: "credential_type", ValidFor: mustDurationFromISO("P1M"), - ValidForISO: ptrTo("P1M"), - EachCredentialValidForISO: ptrTo("P1D"), - IssuanceIntervalISO: ptrTo("P1M"), + ValidForISO: ptr.To("P1M"), + EachCredentialValidForISO: ptr.To("P1D"), + IssuanceIntervalISO: ptr.To("P1M"), Price: decimal.NewFromInt(10), Location: datastore.NullString{ NullString: sql.NullString{ @@ -297,10 +298,6 @@ func TestCreateOrderItem(t *testing.T) { } } -func ptrTo[T any](v T) *T { - return &v -} - func mustDurationFromISO(v string) *time.Duration { result, err := durationFromISO(v) if err != nil { diff --git a/services/wallet/docker-compose.yml b/services/wallet/docker-compose.yml index a887a75ee..9adc43570 100644 --- a/services/wallet/docker-compose.yml +++ b/services/wallet/docker-compose.yml @@ -12,7 +12,7 @@ services: # every time a file changes. wallet-dev-refresh: container_name: wallet-dev-refresh - image: golang:1.18 + image: golang:1.19 ports: - "3353:3353" - "6061:6061" diff --git a/services/wallet/mockservice.go b/services/wallet/mockservice.go index 066f16f49..56fb0791f 100644 --- a/services/wallet/mockservice.go +++ b/services/wallet/mockservice.go @@ -48,3 +48,50 @@ func (mr *MockGeoValidatorMockRecorder) Validate(ctx, geolocation interface{}) * mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockGeoValidator)(nil).Validate), ctx, geolocation) } + +// MockmetricSvc is a mock of metricSvc interface. +type MockmetricSvc struct { + ctrl *gomock.Controller + recorder *MockmetricSvcMockRecorder +} + +// MockmetricSvcMockRecorder is the mock recorder for MockmetricSvc. +type MockmetricSvcMockRecorder struct { + mock *MockmetricSvc +} + +// NewMockmetricSvc creates a new mock instance. +func NewMockmetricSvc(ctrl *gomock.Controller) *MockmetricSvc { + mock := &MockmetricSvc{ctrl: ctrl} + mock.recorder = &MockmetricSvcMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockmetricSvc) EXPECT() *MockmetricSvcMockRecorder { + return m.recorder +} + +// LinkFailureZP mocks base method. +func (m *MockmetricSvc) LinkFailureZP(cc string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "LinkFailureZP", cc) +} + +// LinkFailureZP indicates an expected call of LinkFailureZP. +func (mr *MockmetricSvcMockRecorder) LinkFailureZP(cc interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkFailureZP", reflect.TypeOf((*MockmetricSvc)(nil).LinkFailureZP), cc) +} + +// LinkSuccessZP mocks base method. +func (m *MockmetricSvc) LinkSuccessZP(cc string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "LinkSuccessZP", cc) +} + +// LinkSuccessZP indicates an expected call of LinkSuccessZP. +func (mr *MockmetricSvcMockRecorder) LinkSuccessZP(cc interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkSuccessZP", reflect.TypeOf((*MockmetricSvc)(nil).LinkSuccessZP), cc) +} diff --git a/tools/Dockerfile b/tools/Dockerfile index 4cbf9b384..e1003171a 100644 --- a/tools/Dockerfile +++ b/tools/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.18-alpine as builder +FROM golang:1.19-alpine as builder # put certs in builder image RUN apk update @@ -20,7 +20,7 @@ RUN CGO_ENABLED=0 GOOS=linux go build \ -ldflags "-w -s -X main.version=${VERSION} -X main.buildTime=${BUILD_TIME} -X main.commit=${COMMIT}" \ -o bat-go main.go -FROM alpine:3.15 as base +FROM alpine:3.18 as base # put certs in artifact from builder COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=builder /src/bat-go /bin/