From 56a949241b6ae6182bb8b64eaf3a92324d4ff5c9 Mon Sep 17 00:00:00 2001 From: clD11 <23483715+clD11@users.noreply.github.com> Date: Thu, 10 Feb 2022 17:43:49 +0000 Subject: [PATCH 1/5] production_2022_02_10_1 (#1208) * add balance check before uploading (#1203) * Gemini check status queue (#1205) * feat: handle failed Gemini settlements added response message * feat: handle failed Gemini settlements added test * feat: handle failed Gemini settlements added test * feat: handle failed Gemini settlements added test * feat: handle failed Gemini settlements added test * feat: handle failed Gemini settlements added test * feat: handle failed Gemini settlements added error bundle * feat: handle failed Gemini settlements added error logging * feat: handle failed Gemini settlements fixed typo * feat: handle failed Gemini settlements fmt * feat: handle failed Gemini settlements lint * style: fix variable names and typo (#1207) Co-authored-by: AmirSaber Sharifi --- promotion/datastore.go | 4 +- promotion/drain.go | 64 +++++++++++++------------ promotion/drain_test.go | 86 ++++++++++++++++++++++++++++++++-- settlement/gemini/upload.go | 39 ++++++++++++++- utils/clients/client.go | 29 ++++-------- utils/clients/client_test.go | 44 +++++++++++++++++ utils/clients/gemini/client.go | 9 ++-- utils/errors/errors.go | 29 ++++++++---- utils/errors/errors_test.go | 36 ++++++++++++++ 9 files changed, 273 insertions(+), 67 deletions(-) create mode 100644 utils/clients/client_test.go diff --git a/promotion/datastore.go b/promotion/datastore.go index eaf4dd5a2..adeb761d4 100644 --- a/promotion/datastore.go +++ b/promotion/datastore.go @@ -1568,7 +1568,9 @@ limit 1` txn, err := worker.RedeemAndTransferFunds(ctx, credentials, job.WalletID, job.Total) if err != nil || txn == nil { // log the error from redeem and transfer - logger.Error().Err(err).Msg("failed to redeem and transfer funds") + logger.Error().Err(err). + Interface("claim_drain_id", job.ID). + Msg("failed to redeem and transfer funds") sentry.CaptureException(err) // record as error (retriable or not) diff --git a/promotion/drain.go b/promotion/drain.go index 720f6e4ef..064fa0446 100644 --- a/promotion/drain.go +++ b/promotion/drain.go @@ -663,19 +663,13 @@ func redeemAndTransferBitflyerFunds( return tx, nil } -func redeemAndTransferGeminiFunds( - ctx context.Context, - service *Service, - wallet *walletutils.Info, - total decimal.Decimal, -) (*walletutils.TransactionInfo, error) { - logger := logging.Logger(ctx, "redeemAndTransferGeminiFunds") +func redeemAndTransferGeminiFunds(ctx context.Context, service *Service, wallet *walletutils.Info, + total decimal.Decimal) (*walletutils.TransactionInfo, error) { // in the event that gemini configs or service do not exist // error on redeem and transfer if service.geminiConf == nil || service.geminiClient == nil { - logger.Error().Msg("gemini is misconfigured, missing gemini client and configuration") - return nil, errGeminiMisconfigured + return nil, fmt.Errorf("missing gemini client and configuration: %w", errGeminiMisconfigured) } txType := "drain" @@ -715,7 +709,6 @@ func redeemAndTransferGeminiFunds( signer := cryptography.NewHMACHasher([]byte(service.geminiConf.Secret)) serializedPayload, err := json.Marshal(payload) if err != nil { - logger.Error().Err(err).Msg("failed to serialize payload") return nil, fmt.Errorf("failed to serialize payload: %w", err) } // gemini client will base64 encode the payload prior to sending @@ -727,19 +720,15 @@ func redeemAndTransferGeminiFunds( ) if err != nil { - logger.Error().Err(err).Msg("failed request to gemini") var eb *errorutils.ErrorBundle if errors.As(err, &eb) { - // okay, there was an errorbundle, unwrap and log the error - // convert err.Data() to json and report out - b, err := json.Marshal(eb.Data()) - if err != nil { - logger.Error().Err(err).Msg("failed serialize error bundle data") - } else { - logger.Error().Err(err). - Str("data", string(b)). - Msg("gemini client error details") - } + // retrieve the error bundle data if there is any and log + errorData := eb.DataToString() + logging.FromContext(ctx).Error(). + Err(eb.Cause()). + Str("wallet_id", wallet.ID). + Str("error_bundle", errorData). + Msg("failed to transfer funds gemini") } return nil, fmt.Errorf("failed to transfer funds: %w", err) } @@ -748,13 +737,16 @@ func redeemAndTransferGeminiFunds( // 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 _, v := range *resp { - if strings.ToLower(v.Result) != "ok" { - if v.Reason != nil { - return nil, fmt.Errorf("failed to transfer funds: gemini 'result' is not OK: %s", *v.Reason) - } - return nil, fmt.Errorf("failed to transfer funds: gemini 'result' is not OK") + for _, payout := range *resp { + 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")) } } @@ -855,11 +847,25 @@ func (service *Service) GetGeminiTxnStatus(ctx context.Context, txRef string) (* response, 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") + } return nil, fmt.Errorf("failed to check gemini txn status for %s: %w", txRef, err) } - if response == nil || strings.ToLower(response.Result) == "error" { - return nil, fmt.Errorf("failed to get gemini txn status for %s", txRef) + if response == nil { + return nil, fmt.Errorf("failed to get gemini txn status for %s: response nil", txRef) + } + + if strings.ToLower(response.Result) == "error" { + return nil, fmt.Errorf("failed to get gemini txn status for %s: %s", txRef, + ptr.StringOr(response.Reason, "unknown gemini response error")) } switch strings.ToLower(ptr.String(response.Status)) { diff --git a/promotion/drain_test.go b/promotion/drain_test.go index c9d0c7369..8943e116b 100644 --- a/promotion/drain_test.go +++ b/promotion/drain_test.go @@ -382,7 +382,7 @@ func TestGetGeminiTxnStatus_Response_Nil(t *testing.T) { txStatus, err := service.GetGeminiTxnStatus(ctx, txRef) assert.Nil(t, txStatus) - assert.EqualError(t, err, fmt.Errorf("failed to get gemini txn status for %s", txRef).Error()) + 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) { @@ -414,7 +414,55 @@ func TestGetGeminiTxnStatus_CheckStatus_Error(t *testing.T) { assert.EqualError(t, err, fmt.Errorf("failed to check gemini txn status for %s: %w", txRef, clientError).Error()) } -func TestGetGeminiTxnStatus_ResponseError(t *testing.T) { +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 := rand.Int() + 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, + } + + txStatus, err := service.GetGeminiTxnStatus(ctx, txRef) + + assert.Nil(t, txStatus) + assert.EqualError(t, err, fmt.Errorf("failed to check gemini txn status for %s: %w", txRef, clientError).Error()) +} + +func TestGetGeminiTxnStatus_ResponseError_NoReason(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -442,7 +490,39 @@ func TestGetGeminiTxnStatus_ResponseError(t *testing.T) { txStatus, err := service.GetGeminiTxnStatus(ctx, txRef) assert.Nil(t, txStatus) - assert.EqualError(t, err, fmt.Errorf("failed to get gemini txn status for %s", txRef).Error()) + 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, + } + + txStatus, err := service.GetGeminiTxnStatus(ctx, txRef) + + assert.Nil(t, txStatus) + 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) { diff --git a/settlement/gemini/upload.go b/settlement/gemini/upload.go index 40a7d27d3..e731b1b85 100644 --- a/settlement/gemini/upload.go +++ b/settlement/gemini/upload.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "encoding/hex" "encoding/json" + "fmt" "io/ioutil" "time" @@ -241,6 +242,17 @@ func IterateRequest( submittedTransactions := make(map[string][]settlement.Transaction) + apiSecret, err := appctx.GetStringFromContext(ctx, appctx.GeminiAPISecretCTXKey) + if err != nil { + logger.Error().Err(err).Msg("failed to get gemini api secret") + return submittedTransactions, 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 submittedTransactions, fmt.Errorf("failed to get gemini api key: %w", err) + } + for _, bulkPayoutFile := range bulkPayoutFiles { bytes, err := ioutil.ReadFile(bulkPayoutFile) if err != nil { @@ -259,6 +271,31 @@ func IterateRequest( for i, bulkPayoutRequestRequirements := range geminiBulkPayoutRequestRequirements { blockProgress := geminiComputeTotal(geminiBulkPayoutRequestRequirements[:i+1]) if action == "upload" { + payload, err := json.Marshal(gemini.NewBalancesPayload(nil)) + if err != nil { + logger.Error().Err(err).Msg("failed unmarshal balance payload") + return submittedTransactions, err + } + + signer := cryptography.NewHMACHasher([]byte(apiSecret)) + result, err := geminiClient.FetchBalances(ctx, apiKey, signer, string(payload)) + availableCurrency := map[string]decimal.Decimal{} + for _, currency := range *result { + availableCurrency[currency.Currency] = currency.Amount + } + + requiredCurrency := map[string]decimal.Decimal{} + for _, pay := range bulkPayoutRequestRequirements.Base.Payouts { + requiredCurrency[pay.Currency] = requiredCurrency[pay.Currency].Add(pay.Amount) + } + + for key, amount := range requiredCurrency { + if availableCurrency[key].LessThan(amount) { + logger.Error().Str("required", amount.String()).Str("available", availableCurrency[key].String()).Str("currency", key).Err(err).Msg("failed to meet required balance") + return submittedTransactions, fmt.Errorf("failed to meet required balance: %w", err) + } + } + submittedTransactions, err = SubmitBulkPayoutTransactions( ctx, transactionsMap, @@ -284,7 +321,7 @@ func IterateRequest( blockProgress, ) if err != nil { - logger.Error().Err(err).Msg("falied to check payout transactions status") + logger.Error().Err(err).Msg("failed to check payout transactions status") return nil, err } } diff --git a/utils/clients/client.go b/utils/clients/client.go index 75823020b..205a4b4c2 100644 --- a/utils/clients/client.go +++ b/utils/clients/client.go @@ -217,11 +217,7 @@ func (c *SimpleHTTPClient) NewRequest( } // Do the specified http request, decoding the JSON result into v -func (c *SimpleHTTPClient) do( - ctx context.Context, - req *http.Request, - v interface{}, -) (*http.Response, error) { +func (c *SimpleHTTPClient) do(ctx context.Context, req *http.Request, v interface{}) (*http.Response, error) { // concurrent client request instrumentation concurrentClientRequests.With( @@ -297,33 +293,28 @@ func (c *SimpleHTTPClient) do( // Do the specified http request, decoding the JSON result into v func (c *SimpleHTTPClient) Do(ctx context.Context, req *http.Request, v interface{}) (*http.Response, error) { - var ( - code int - resp, err = c.do(ctx, req, v) - ) - if resp != nil { - // it is possible to have a nil resp from c.do... - code = resp.StatusCode - } + resp, err := c.do(ctx, req, v) if err != nil { // errors returned from c.do could be go errors or upstream api errors if resp != nil { // if there was an error from the service, read the response body // and inject into error for later - b, err := ioutil.ReadAll(resp.Body) + b, _ := ioutil.ReadAll(resp.Body) rb := string(b) resp.Body = ioutil.NopCloser(bytes.NewBuffer(b)) - return resp, NewHTTPError(err, req.URL.String(), "response", code, struct { - Body interface{} + errorData := struct { ResponseHeaders interface{} + Body interface{} }{ // put response body/headers in the err state data - Body: rb, ResponseHeaders: resp.Header, - }) + Body: rb, + } + + return resp, NewHTTPError(err, req.URL.String(), "response", resp.StatusCode, errorData) } - return resp, fmt.Errorf("failed c.do, no response body: %w", err) + return nil, fmt.Errorf("failed c.do, no response body: %w", err) } return resp, nil } diff --git a/utils/clients/client_test.go b/utils/clients/client_test.go new file mode 100644 index 000000000..cd27965c0 --- /dev/null +++ b/utils/clients/client_test.go @@ -0,0 +1,44 @@ +package clients + +import ( + "context" + "fmt" + "github.com/brave-intl/bat-go/utils/errors" + testutils "github.com/brave-intl/bat-go/utils/test" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "testing" +) + +func TestDo_ErrorWithResponse(t *testing.T) { + errorMsg := testutils.RandomString() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(errorMsg)) + assert.NoError(t, err) + })) + defer ts.Close() + + req, err := http.NewRequest(http.MethodGet, ts.URL, nil) + assert.NoError(t, err) + + client, err := New(ts.URL, "") + assert.NoError(t, err) + + // pass data as invalid result type to cause error + var data *string + response, err := client.Do(context.Background(), req, data) + + assert.IsType(t, &errors.ErrorBundle{}, err) + assert.NotNil(t, response) + + actual := err.(*errors.ErrorBundle) + assert.Equal(t, "response", actual.Error()) + assert.NotNil(t, actual.Cause(), ErrUnableToDecode) + + httpState := actual.Data().(HTTPState) + assert.Equal(t, httpState.Status, http.StatusOK) + assert.Equal(t, ts.URL, httpState.Path) + assert.Contains(t, fmt.Sprintf("+%v", httpState.Body), errorMsg) +} diff --git a/utils/clients/gemini/client.go b/utils/clients/gemini/client.go index 431e035a0..d1db459e4 100644 --- a/utils/clients/gemini/client.go +++ b/utils/clients/gemini/client.go @@ -390,12 +390,8 @@ func (c *HTTPClient) CheckTxStatus(ctx context.Context, APIKey string, clientID } // UploadBulkPayout uploads the bulk payout for gemini -func (c *HTTPClient) UploadBulkPayout( - ctx context.Context, - APIKey string, - signer cryptography.HMACKey, - payload string, -) (*[]PayoutResult, error) { +func (c *HTTPClient) UploadBulkPayout(ctx context.Context, APIKey string, signer cryptography.HMACKey, payload string) (*[]PayoutResult, error) { + req, err := c.client.NewRequest(ctx, "POST", "/v1/payments/bulkPay", nil, nil) if err != nil { return nil, err @@ -410,6 +406,7 @@ func (c *HTTPClient) UploadBulkPayout( if err != nil { return nil, err } + return &body, err } diff --git a/utils/errors/errors.go b/utils/errors/errors.go index 17cd3abbd..718c15795 100644 --- a/utils/errors/errors.go +++ b/utils/errors/errors.go @@ -1,6 +1,7 @@ package errors import ( + "encoding/json" "errors" "fmt" ) @@ -53,23 +54,35 @@ func New(cause error, message string, data interface{}) error { } // Data from error origin -func (err ErrorBundle) Data() interface{} { - return err.data +func (e ErrorBundle) Data() interface{} { + return e.data } // Cause returns the associated cause -func (err ErrorBundle) Cause() error { - return err.cause +func (e ErrorBundle) Cause() error { + return e.cause } // Unwrap returns the associated cause -func (err ErrorBundle) Unwrap() error { - return err.cause +func (e ErrorBundle) Unwrap() error { + return e.cause } // Error turns into an error -func (err ErrorBundle) Error() string { - return err.message +func (e ErrorBundle) Error() string { + return e.message +} + +// DataToString returns string representation of data +func (e ErrorBundle) DataToString() string { + if e.data == nil { + return "no error bundle data" + } + b, err := json.Marshal(e.data) + if err != nil { + return fmt.Sprintf("error retrieving error bundle data %s", err.Error()) + } + return string(b) } // Wrap wraps an error diff --git a/utils/errors/errors_test.go b/utils/errors/errors_test.go index 567eae737..53cefec35 100644 --- a/utils/errors/errors_test.go +++ b/utils/errors/errors_test.go @@ -1,10 +1,14 @@ package errors_test import ( + "encoding/json" "errors" "fmt" "testing" + testutils "github.com/brave-intl/bat-go/utils/test" + "github.com/stretchr/testify/assert" + errutil "github.com/brave-intl/bat-go/utils/errors" ) @@ -46,3 +50,35 @@ func TestMultiErrorUnwrap(t *testing.T) { t.Error("failed to unwrap multierror correctly: not 'is' err2") } } + +func TestErrorBundle_DataToString_DataNil(t *testing.T) { + err := errutil.Wrap(errors.New(testutils.RandomString()), testutils.RandomString()) + var actual *errutil.ErrorBundle + errors.As(err, &actual) + assert.Equal(t, "no error bundle data", actual.DataToString()) +} + +func TestErrorBundle_DataToString_MarshallError(t *testing.T) { + unsupportedData := func() {} + sut := errutil.New(errors.New(testutils.RandomString()), testutils.RandomString(), unsupportedData) + + expected := "error retrieving error bundle data" + + var actual *errutil.ErrorBundle + errors.As(sut, &actual) + + assert.Contains(t, actual.DataToString(), expected) +} + +func TestErrorBundle_DataToString(t *testing.T) { + errorData := testutils.RandomString() + sut := errutil.New(errors.New(testutils.RandomString()), testutils.RandomString(), errorData) + + expected, err := json.Marshal(errorData) + assert.NoError(t, err) + + var actual *errutil.ErrorBundle + errors.As(sut, &actual) + + assert.Equal(t, string(expected), actual.DataToString()) +} From 4be3be493411d06b91f295e760b6a882704c7de0 Mon Sep 17 00:00:00 2001 From: clD11 <23483715+clD11@users.noreply.github.com> Date: Fri, 8 Apr 2022 16:50:38 +0100 Subject: [PATCH 2/5] prod_2022_04_08_01 (#1372) * bug: gemini deposit destination fix (#1370) uses claim drain deposit destination everywhere * Uphold settlement bug fixes (#1363) * Fix Progress Update * Add status logging * Replace confusing nil error log with resubmission notice * Add confirmation success log * Break progress detection loop once a match is found. Co-authored-by: Jackson Egan --- cmd/settlement/uphold.go | 9 ++++++-- promotion/datastore.go | 2 +- promotion/datastore_test.go | 8 +++---- promotion/drain.go | 42 +++++++++++++++++++++---------------- promotion/mockdrain.go | 18 ++++++++-------- settlement/settlement.go | 4 ++-- 6 files changed, 47 insertions(+), 36 deletions(-) diff --git a/cmd/settlement/uphold.go b/cmd/settlement/uphold.go index 17c0cab2f..d0baeaff9 100644 --- a/cmd/settlement/uphold.go +++ b/cmd/settlement/uphold.go @@ -279,8 +279,9 @@ func UpholdUpload( } if existingProgressEntry.Message == progressMessage { - existingProgressEntry.Count++ - } else if p == len(progress.Progress) { + progress.Progress[p].Count++ + break + } else if p == len(progress.Progress)-1 { progress.Progress = append(progress.Progress, logging.UpholdProgress{ Message: progressMessage, Count: 1, @@ -299,6 +300,7 @@ func UpholdUpload( settlementTransaction := &settlementState.Transactions[i] if settlementTransaction.IsProcessing() { + logger.Info().Msg("reattempting to confirm transaction in progress") // Confirm will first check if the transaction has already been confirmed err = settlement.ConfirmPreparedTransaction(ctx, settlementWallet, settlementTransaction, true) if err != nil { @@ -326,16 +328,19 @@ func UpholdUpload( transactionsMap := make(map[string][]settlement.Transaction) for i := 0; i < len(settlementState.Transactions); i++ { + logger.Info().Msg("redacting transactions in log files") // Redact signed transactions settlementState.Transactions[i].SignedTx = "" // Group by status + logger.Info().Msg("grouping transactions by status") status := settlementState.Transactions[i].Status transactionsMap[status] = append(transactionsMap[status], settlementState.Transactions[i]) } for key, txs := range transactionsMap { outputFile := outputFilePrefix + "-" + key + ".json" + logger.Info().Msg(fmt.Sprintf("writing out transactions to %s for eyeshade", outputFile)) // Write out transactions ready to be submitted to eyeshade out, err := json.MarshalIndent(txs, "", " ") diff --git a/promotion/datastore.go b/promotion/datastore.go index 1987393a3..0bedecbd2 100644 --- a/promotion/datastore.go +++ b/promotion/datastore.go @@ -1598,7 +1598,7 @@ limit 1` ctx = context.WithValue(ctx, appctx.SkipRedeemCredentialsCTXKey, true) } - txn, err := worker.RedeemAndTransferFunds(ctx, credentials, job.WalletID, job.Total, job.ClaimID) + txn, err := worker.RedeemAndTransferFunds(ctx, credentials, job) if err != nil || txn == nil { // log the error from redeem and transfer logger.Error().Err(err). diff --git a/promotion/datastore_test.go b/promotion/datastore_test.go index e7091232f..6f4bd0711 100644 --- a/promotion/datastore_test.go +++ b/promotion/datastore_test.go @@ -926,7 +926,7 @@ func (suite *PostgresTestSuite) TestDrainClaim() { mockDrainWorker := NewMockDrainWorker(mockCtrl) // One drain job should run - mockDrainWorker.EXPECT().RedeemAndTransferFunds(gomock.Any(), gomock.Eq(credentials), gomock.Eq(walletID), testutils.DecEq(total), &claim.ID).Return(nil, errors.New("Worker failed")) + 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) @@ -1031,7 +1031,7 @@ func (suite *PostgresTestSuite) TestRunNextDrainJob_Gemini_Claim() { drainWorker := NewMockDrainWorker(ctrl) drainWorker.EXPECT(). - RedeemAndTransferFunds(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + RedeemAndTransferFunds(gomock.Any(), gomock.Any(), gomock.Any()). Return(&transactionInfo, nil) attempted, err := pg.RunNextDrainJob(context.Background(), drainWorker) @@ -1203,7 +1203,7 @@ func (suite *PostgresTestSuite) TestRunNextBatchPaymentsJob_NextDrainJob_Concurr drainWorker := NewMockDrainWorker(ctrl) drainWorker.EXPECT(). - RedeemAndTransferFunds(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + RedeemAndTransferFunds(gomock.Any(), gomock.Any(), gomock.Any()). Return(&transactionInfo, nil). Times(3) @@ -1454,7 +1454,7 @@ func (suite *PostgresTestSuite) TestRunNextDrainJob_CBRBypass_ManualRetry() { ctx := context.Background() drainWorker.EXPECT(). - RedeemAndTransferFunds(isCBRBypass(ctx), credentialRedemptions, walletID, decimal.New(1, 0), gomock.Any()). + RedeemAndTransferFunds(isCBRBypass(ctx), credentialRedemptions, gomock.Any()). Return(&walletutils.TransactionInfo{}, nil) attempted, err := pg.RunNextDrainJob(ctx, drainWorker) diff --git a/promotion/drain.go b/promotion/drain.go index feaca3bab..bccc0e390 100644 --- a/promotion/drain.go +++ b/promotion/drain.go @@ -254,7 +254,7 @@ type DrainPoll struct { // 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, walletID uuid.UUID, total decimal.Decimal, claimID *uuid.UUID) (*walletutils.TransactionInfo, error) + RedeemAndTransferFunds(ctx context.Context, credentials []cbr.CredentialRedemption, drainJob DrainJob) (*walletutils.TransactionInfo, error) } // DrainRetryWorker - reads walletID @@ -484,11 +484,12 @@ func (service *Service) SubmitBatchTransfer(ctx context.Context, batchID *uuid.U } // RedeemAndTransferFunds after validating that all the credential bindings -func (service *Service) RedeemAndTransferFunds(ctx context.Context, credentials []cbr.CredentialRedemption, walletID uuid.UUID, total decimal.Decimal, claimID *uuid.UUID) (*walletutils.TransactionInfo, error) { +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, walletID) + 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 @@ -525,7 +526,7 @@ func (service *Service) RedeemAndTransferFunds(ctx context.Context, credentials // 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, walletID.String()); err != nil { + 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) } @@ -536,7 +537,7 @@ func (service *Service) RedeemAndTransferFunds(ctx context.Context, credentials 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(&walletID, claimID) + 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) @@ -548,7 +549,7 @@ func (service *Service) RedeemAndTransferFunds(ctx context.Context, credentials } // perform reputation check for wallet, and error accordingly if there is a reputation failure - reputable, cohorts, err := service.reputationClient.IsDrainReputable(ctx, walletID, *promotionID, withdrawalAmount) + 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 @@ -572,7 +573,7 @@ func (service *Service) RedeemAndTransferFunds(ctx context.Context, credentials } else { // legacy behavior // perform reputation check for wallet, and error accordingly if there is a reputation failure - reputable, err := service.reputationClient.IsWalletAdsReputable(ctx, walletID, "") + 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 @@ -586,7 +587,7 @@ func (service *Service) RedeemAndTransferFunds(ctx context.Context, credentials if *wallet.UserDepositAccountProvider == "uphold" { // FIXME should use idempotency key - tx, err := service.hotWallet.Transfer(ctx, altcurrency.BAT, altcurrency.BAT.ToProbi(total), wallet.UserDepositDestination) + 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) } @@ -595,9 +596,9 @@ func (service *Service) RedeemAndTransferFunds(ctx context.Context, credentials } return tx, err } else if *wallet.UserDepositAccountProvider == "bitflyer" { - return redeemAndTransferBitflyerFunds(ctx, service, wallet, total) + return redeemAndTransferBitflyerFunds(ctx, service, wallet, drainJob.Total) } else if *wallet.UserDepositAccountProvider == "gemini" { - return redeemAndTransferGeminiFunds(ctx, service, wallet, total) + return redeemAndTransferGeminiFunds(ctx, service, drainJob.Total, drainJob) } else if *wallet.UserDepositAccountProvider == "brave" { // update the mint job for this walletID promoTotal := map[string]decimal.Decimal{} @@ -619,7 +620,7 @@ func (service *Service) RedeemAndTransferFunds(ctx context.Context, credentials 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, walletID, promotionID, v) + err = service.Datastore.SetMintDrainPromotionTotal(ctx, drainJob.WalletID, promotionID, v) if err != nil { return nil, fmt.Errorf("failed to set append total funds: %w", err) } @@ -659,8 +660,7 @@ func redeemAndTransferBitflyerFunds( return tx, nil } -func redeemAndTransferGeminiFunds(ctx context.Context, service *Service, wallet *walletutils.Info, - total decimal.Decimal) (*walletutils.TransactionInfo, error) { +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 @@ -672,16 +672,21 @@ func redeemAndTransferGeminiFunds(ctx context.Context, service *Service, wallet 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 = wallet.UserDepositDestination + tx.Destination = depositDestination tx.DestAmount = total tx.Status = txnStatusGeminiPending settlementTx := settlement.Transaction{ SettlementID: transferID, Type: txType, - Destination: wallet.UserDepositDestination, + Destination: depositDestination, Channel: channel, } @@ -691,7 +696,7 @@ func redeemAndTransferGeminiFunds(ctx context.Context, service *Service, wallet TxRef: gemini.GenerateTxRef(&settlementTx), Amount: total, Currency: "BAT", - Destination: wallet.UserDepositDestination, + Destination: depositDestination, Account: &account, }, } @@ -722,7 +727,7 @@ func redeemAndTransferGeminiFunds(ctx context.Context, service *Service, wallet errorData := eb.DataToString() logging.FromContext(ctx).Error(). Err(eb.Cause()). - Str("wallet_id", wallet.ID). + Interface("wallet_id", drainJob.WalletID). Str("error_bundle", errorData). Msg("failed to transfer funds gemini") } @@ -738,7 +743,8 @@ func redeemAndTransferGeminiFunds(ctx context.Context, service *Service, wallet for _, payout := range *resp { logging.FromContext(ctx).Info(). - Str("wallet_id", wallet.ID). + 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")). diff --git a/promotion/mockdrain.go b/promotion/mockdrain.go index d11f28893..12960a1bf 100644 --- a/promotion/mockdrain.go +++ b/promotion/mockdrain.go @@ -11,7 +11,7 @@ import ( cbr "github.com/brave-intl/bat-go/utils/clients/cbr" wallet "github.com/brave-intl/bat-go/utils/wallet" gomock "github.com/golang/mock/gomock" - uuid "github.com/satori/go.uuid" + go_uuid "github.com/satori/go.uuid" decimal "github.com/shopspring/decimal" ) @@ -39,18 +39,18 @@ func (m *MockDrainWorker) EXPECT() *MockDrainWorkerMockRecorder { } // RedeemAndTransferFunds mocks base method. -func (m *MockDrainWorker) RedeemAndTransferFunds(ctx context.Context, credentials []cbr.CredentialRedemption, walletID uuid.UUID, total decimal.Decimal, claimID *uuid.UUID) (*wallet.TransactionInfo, error) { +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, walletID, total, claimID) + 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, walletID, total, claimID interface{}) *gomock.Call { +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, walletID, total, claimID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RedeemAndTransferFunds", reflect.TypeOf((*MockDrainWorker)(nil).RedeemAndTransferFunds), ctx, credentials, drainJob) } // MockDrainRetryWorker is a mock of DrainRetryWorker interface. @@ -77,10 +77,10 @@ func (m *MockDrainRetryWorker) EXPECT() *MockDrainRetryWorkerMockRecorder { } // FetchAdminAttestationWalletID mocks base method. -func (m *MockDrainRetryWorker) FetchAdminAttestationWalletID(ctx context.Context) (*uuid.UUID, error) { +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].(*uuid.UUID) + ret0, _ := ret[0].(*go_uuid.UUID) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -115,7 +115,7 @@ func (m *MockMintWorker) EXPECT() *MockMintWorkerMockRecorder { } // MintGrant mocks base method. -func (m *MockMintWorker) MintGrant(ctx context.Context, walletID uuid.UUID, total decimal.Decimal, promoIDs ...uuid.UUID) error { +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 { @@ -157,7 +157,7 @@ func (m *MockBatchTransferWorker) EXPECT() *MockBatchTransferWorkerMockRecorder } // SubmitBatchTransfer mocks base method. -func (m *MockBatchTransferWorker) SubmitBatchTransfer(ctx context.Context, batchID *uuid.UUID) error { +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) diff --git a/settlement/settlement.go b/settlement/settlement.go index a40968eab..3f92d5395 100644 --- a/settlement/settlement.go +++ b/settlement/settlement.go @@ -383,6 +383,7 @@ func ConfirmPreparedTransaction( } if isResubmit { + logger.Info().Msg(fmt.Sprintf("attempting resubmission of transaction for destination: %s", settlement.Destination)) // first check if the transaction has already been confirmed upholdInfo, err := settlementWallet.GetTransaction(ctx, settlement.ProviderID) if err == nil { @@ -404,12 +405,11 @@ func ConfirmPreparedTransaction( return nil } } - } else { - logger.Info().Msg(fmt.Sprintf("error retrieving referenced transaction: %s", err)) } settlementInfo, err = settlementWallet.ConfirmTransaction(ctx, settlement.ProviderID) if err == nil { + logger.Info().Msg(fmt.Sprintf("transaction confirmed for destination: %s", settlement.Destination)) settlement.Status = settlementInfo.Status settlement.Currency = settlementInfo.DestCurrency settlement.Amount = settlementInfo.DestAmount From 252406a4eeaafd33302500dc8411a33330559120 Mon Sep 17 00:00:00 2001 From: Pavel Brm <5097196+pavelbrm@users.noreply.github.com> Date: Fri, 13 Oct 2023 02:08:35 +1300 Subject: [PATCH 3/5] Revert "Production 2023-11-10_01" --- services/skus/controllers.go | 38 +++--- services/skus/service.go | 232 +++++++++++++++++------------------ 2 files changed, 126 insertions(+), 144 deletions(-) diff --git a/services/skus/controllers.go b/services/skus/controllers.go index 93acffd41..8dc8750ca 100644 --- a/services/skus/controllers.go +++ b/services/skus/controllers.go @@ -841,54 +841,48 @@ func MerchantTransactions(service *Service) handlers.AppHandler { // VerifyCredentialV2 - version 2 of verify credential func VerifyCredentialV2(service *Service) handlers.AppHandler { - return func(w http.ResponseWriter, r *http.Request) *handlers.AppError { - + return handlers.AppHandler(func(w http.ResponseWriter, r *http.Request) *handlers.AppError { ctx := r.Context() - l := logging.Logger(ctx, "VerifyCredentialV2") + logger := logging.Logger(ctx, "VerifyCredentialV2") + logger.Debug().Msg("starting VerifyCredentialV2 controller") var req = new(VerifyCredentialRequestV2) if err := inputs.DecodeAndValidateReader(ctx, req, r.Body); err != nil { - l.Error().Err(err).Msg("failed to read request") + logger.Error().Err(err).Msg("failed to read request") return handlers.WrapError(err, "Error in request body", http.StatusBadRequest) } - appErr := service.verifyCredential(ctx, req, w) - if appErr != nil { - l.Error().Err(appErr).Msg("failed to verify credential") - } - - return appErr - } + return service.verifyCredential(ctx, req, w) + }) } // VerifyCredentialV1 is the handler for verifying subscription credentials func VerifyCredentialV1(service *Service) handlers.AppHandler { - return func(w http.ResponseWriter, r *http.Request) *handlers.AppError { + return handlers.AppHandler(func(w http.ResponseWriter, r *http.Request) *handlers.AppError { ctx := r.Context() - l := logging.Logger(r.Context(), "VerifyCredentialV1") + + logger := logging.Logger(r.Context(), "VerifyCredentialV1") + logger.Debug().Msg("starting VerifyCredentialV1 controller") var req = new(VerifyCredentialRequestV1) err := requestutils.ReadJSON(r.Context(), r.Body, &req) if err != nil { - l.Error().Err(err).Msg("failed to read request") + logger.Error().Err(err).Msg("failed to read request") return handlers.WrapError(err, "Error in request body", http.StatusBadRequest) } - l.Debug().Msg("read verify credential post body") + logger.Debug().Msg("read verify credential post body") _, err = govalidator.ValidateStruct(req) if err != nil { - l.Error().Err(err).Msg("failed to validate request") + logger.Error().Err(err).Msg("failed to validate request") return handlers.WrapError(err, "Error in request validation", http.StatusBadRequest) } - appErr := service.verifyCredential(ctx, req, w) - if appErr != nil { - l.Error().Err(appErr).Msg("failed to verify credential") - } + logger.Debug().Msg("validated verify credential post body") - return appErr - } + return service.verifyCredential(ctx, req, w) + }) } // WebhookRouter - handles calls from various payment method webhooks informing payments of completion diff --git a/services/skus/service.go b/services/skus/service.go index 0504b49b0..d2f54b112 100644 --- a/services/skus/service.go +++ b/services/skus/service.go @@ -1297,8 +1297,9 @@ type credential interface { GetPresentation(context.Context) string } +// TODO refactor this see issue #1502 // verifyCredential - given a credential, verify it. -func (s *Service) verifyCredential(ctx context.Context, cred credential, w http.ResponseWriter) *handlers.AppError { +func (s *Service) verifyCredential(ctx context.Context, req credential, w http.ResponseWriter) *handlers.AppError { logger := logging.Logger(ctx, "verifyCredential") merchant, err := GetMerchant(ctx) @@ -1311,9 +1312,9 @@ func (s *Service) verifyCredential(ctx context.Context, cred credential, w http. caveats := GetCaveats(ctx) - if cred.GetMerchantID(ctx) != merchant { + if req.GetMerchantID(ctx) != merchant { logger.Warn(). - Str("req.MerchantID", cred.GetMerchantID(ctx)). + Str("req.MerchantID", req.GetMerchantID(ctx)). Str("merchant", merchant). Msg("merchant does not match the key's merchant") return handlers.WrapError(nil, "Verify request merchant does not match authentication", http.StatusForbidden) @@ -1323,9 +1324,9 @@ func (s *Service) verifyCredential(ctx context.Context, cred credential, w http. if caveats != nil { if sku, ok := caveats["sku"]; ok { - if cred.GetSku(ctx) != sku { + if req.GetSku(ctx) != sku { logger.Warn(). - Str("req.SKU", cred.GetSku(ctx)). + Str("req.SKU", req.GetSku(ctx)). Str("sku", sku). Msg("sku caveat does not match") return handlers.WrapError(nil, "Verify request sku does not match authentication", http.StatusForbidden) @@ -1334,97 +1335,130 @@ func (s *Service) verifyCredential(ctx context.Context, cred credential, w http. } logger.Debug().Msg("caveats validated") - kind := cred.GetType(ctx) - switch kind { - case singleUse, timeLimitedV2: - return s.verifyBlindedTokenCredential(ctx, cred, w) - case timeLimited: - return s.verifyTimeLimitedV1Credential(ctx, cred, w) - default: - return handlers.WrapError(nil, "Unknown credential type", http.StatusBadRequest) - } -} - -// verifyBlindedTokenCredential verifies a single use or time limited v2 credential. -func (s *Service) verifyBlindedTokenCredential(ctx context.Context, req credential, w http.ResponseWriter) *handlers.AppError { - bytes, err := base64.StdEncoding.DecodeString(req.GetPresentation(ctx)) - if err != nil { - return handlers.WrapError(err, "Error in decoding presentation", http.StatusBadRequest) - } - - decodedCred := &cbr.CredentialRedemption{} - if err := json.Unmarshal(bytes, decodedCred); err != nil { - return handlers.WrapError(err, "Error in presentation formatting", http.StatusBadRequest) - } + if req.GetType(ctx) == singleUse || req.GetType(ctx) == timeLimitedV2 { + var bytes []byte + bytes, err = base64.StdEncoding.DecodeString(req.GetPresentation(ctx)) + if err != nil { + return handlers.WrapError(err, "Error in decoding presentation", http.StatusBadRequest) + } - // Ensure that the credential being redeemed (opaque to merchant) matches the outer credential details. - issuerID, err := encodeIssuerID(req.GetMerchantID(ctx), req.GetSku(ctx)) - if err != nil { - return handlers.WrapError(err, "Error in outer merchantId or sku", http.StatusBadRequest) - } + var decodedCredential cbr.CredentialRedemption + err = json.Unmarshal(bytes, &decodedCredential) + if err != nil { + return handlers.WrapError(err, "Error in presentation formatting", http.StatusBadRequest) + } - if issuerID != decodedCred.Issuer { - return handlers.WrapError(nil, "Error, outer merchant and sku don't match issuer", http.StatusBadRequest) - } + // Ensure that the credential being redeemed (opaque to merchant) matches the outer credential details + issuerID, err := encodeIssuerID(req.GetMerchantID(ctx), req.GetSku(ctx)) + if err != nil { + return handlers.WrapError(err, "Error in outer merchantId or sku", http.StatusBadRequest) + } + if issuerID != decodedCredential.Issuer { + return handlers.WrapError(nil, "Error, outer merchant and sku don't match issuer", http.StatusBadRequest) + } - return s.redeemBlindedCred(ctx, w, req.GetType(ctx), decodedCred) -} + switch req.GetType(ctx) { + case singleUse: + err = s.cbClient.RedeemCredential(ctx, decodedCredential.Issuer, decodedCredential.TokenPreimage, + decodedCredential.Signature, decodedCredential.Issuer) + case timeLimitedV2: + err = s.cbClient.RedeemCredentialV3(ctx, decodedCredential.Issuer, decodedCredential.TokenPreimage, + decodedCredential.Signature, decodedCredential.Issuer) + default: + return handlers.WrapError(fmt.Errorf("credential type %s not suppoted", req.GetType(ctx)), + "unknown credential type %s", http.StatusBadRequest) + } -// verifyTimeLimitedV1Credential verifies a time limited v1 credential. -func (s *Service) verifyTimeLimitedV1Credential(ctx context.Context, req credential, w http.ResponseWriter) *handlers.AppError { - data, err := base64.StdEncoding.DecodeString(req.GetPresentation(ctx)) - if err != nil { - return handlers.WrapError(err, "Error in decoding presentation", http.StatusBadRequest) - } + if err != nil { + // if this is a duplicate redemption these are not verified + if err.Error() == cbr.ErrDupRedeem.Error() || err.Error() == cbr.ErrBadRequest.Error() { + return handlers.WrapError(err, "invalid credentials", http.StatusForbidden) + } + return handlers.WrapError(err, "Error verifying credentials", http.StatusInternalServerError) + } - present := &tlv1CredPresentation{} - if err := json.Unmarshal(data, present); err != nil { - return handlers.WrapError(err, "Error in presentation formatting", http.StatusBadRequest) + return handlers.RenderContent(ctx, "Credentials successfully verified", w, http.StatusOK) } - merchID := req.GetMerchantID(ctx) + if req.GetType(ctx) == "time-limited" { + // Presentation includes a token and token metadata test test + type Presentation struct { + IssuedAt string `json:"issuedAt"` + ExpiresAt string `json:"expiresAt"` + Token string `json:"token"` + } - // Ensure that the credential being redeemed (opaque to merchant) matches the outer credential details. - issuerID, err := encodeIssuerID(merchID, req.GetSku(ctx)) - if err != nil { - return handlers.WrapError(err, "Error in outer merchantId or sku", http.StatusBadRequest) - } + var bytes []byte + bytes, err = base64.StdEncoding.DecodeString(req.GetPresentation(ctx)) + if err != nil { + logger.Error().Err(err). + Msg("failed to decode the request token presentation") + return handlers.WrapError(err, "Error in decoding presentation", http.StatusBadRequest) + } + logger.Debug().Str("presentation", string(bytes)).Msg("presentation decoded") - keys, err := s.GetCredentialSigningKeys(ctx, merchID) - if err != nil { - return handlers.WrapError(err, "failed to get merchant signing key", http.StatusInternalServerError) - } + var presentation Presentation + err = json.Unmarshal(bytes, &presentation) + if err != nil { + logger.Error().Err(err). + Msg("failed to unmarshal the request token presentation") + return handlers.WrapError(err, "Error in presentation formatting", http.StatusBadRequest) + } - issuedAt, err := time.Parse("2006-01-02", present.IssuedAt) - if err != nil { - return handlers.WrapError(err, "Error parsing issuedAt", http.StatusBadRequest) - } + logger.Debug().Str("presentation", string(bytes)).Msg("presentation unmarshalled") - expiresAt, err := time.Parse("2006-01-02", present.ExpiresAt) - if err != nil { - return handlers.WrapError(err, "Error parsing expiresAt", http.StatusBadRequest) - } + // Ensure that the credential being redeemed (opaque to merchant) matches the outer credential details + issuerID, err := encodeIssuerID(req.GetMerchantID(ctx), req.GetSku(ctx)) + if err != nil { + logger.Error().Err(err). + Msg("failed to encode the issuer id") + return handlers.WrapError(err, "Error in outer merchantId or sku", http.StatusBadRequest) + } + logger.Debug().Str("issuer", issuerID).Msg("issuer encoded") - for _, key := range keys { - timeLimitedSecret := cryptography.NewTimeLimitedSecret(key) + keys, err := s.GetCredentialSigningKeys(ctx, req.GetMerchantID(ctx)) + if err != nil { + return handlers.WrapError(err, "failed to get merchant signing key", http.StatusInternalServerError) + } - verified, err := timeLimitedSecret.Verify([]byte(issuerID), issuedAt, expiresAt, present.Token) + issuedAt, err := time.Parse("2006-01-02", presentation.IssuedAt) + if err != nil { + logger.Error().Err(err). + Msg("failed to parse issued at time of credential") + return handlers.WrapError(err, "Error parsing issuedAt", http.StatusBadRequest) + } + expiresAt, err := time.Parse("2006-01-02", presentation.ExpiresAt) if err != nil { - return handlers.WrapError(err, "Error in token verification", http.StatusBadRequest) + logger.Error().Err(err). + Msg("failed to parse expires at time of credential") + return handlers.WrapError(err, "Error parsing expiresAt", http.StatusBadRequest) } - if verified { - // Check against expiration time, issued time. - now := time.Now() - if now.After(expiresAt) || now.Before(issuedAt) { - return handlers.WrapError(nil, "Credentials are not valid", http.StatusForbidden) + for _, key := range keys { + timeLimitedSecret := cryptography.NewTimeLimitedSecret(key) + verified, err := timeLimitedSecret.Verify([]byte(issuerID), issuedAt, expiresAt, presentation.Token) + if err != nil { + logger.Error().Err(err). + Msg("failed to verify time limited credential") + return handlers.WrapError(err, "Error in token verification", http.StatusBadRequest) } - return handlers.RenderContent(ctx, "Credentials successfully verified", w, http.StatusOK) + if verified { + // check against expiration time, issued time + if time.Now().After(expiresAt) || time.Now().Before(issuedAt) { + logger.Error(). + Msg("credentials are not valid") + return handlers.RenderContent(ctx, "Credentials are not valid", w, http.StatusForbidden) + } + logger.Debug().Msg("credentials verified") + return handlers.RenderContent(ctx, "Credentials successfully verified", w, http.StatusOK) + } } + logger.Error(). + Msg("credentials could not be verified") + return handlers.RenderContent(ctx, "Credentials could not be verified", w, http.StatusForbidden) } - - return handlers.WrapError(nil, "Credentials could not be verified", http.StatusForbidden) + return handlers.WrapError(nil, "Unknown credential type", http.StatusBadRequest) } // RunSendSigningRequestJob - send the order credentials signing requests @@ -1772,41 +1806,6 @@ func (s *Service) createStripeSessID(ctx context.Context, req *model.CreateOrder return nil } -func (s *Service) redeemBlindedCred(ctx context.Context, w http.ResponseWriter, kind string, cred *cbr.CredentialRedemption) *handlers.AppError { - var redeemFn func(ctx context.Context, issuer, preimage, signature, payload string) error - - switch kind { - case singleUse: - redeemFn = s.cbClient.RedeemCredential - case timeLimitedV2: - redeemFn = s.cbClient.RedeemCredentialV3 - default: - return handlers.WrapError(fmt.Errorf("credential type %s not suppoted", kind), "unknown credential type %s", http.StatusBadRequest) - } - - // FIXME: we shouldn't be using the issuer as the payload, it ideally would be a unique request identifier - // to allow for more flexible idempotent behavior. - if err := redeemFn(ctx, cred.Issuer, cred.TokenPreimage, cred.Signature, cred.Issuer); err != nil { - msg := err.Error() - - // Time limited v2: Expose a credential id so the caller can decide whether to allow multiple redemptions. - if kind == timeLimitedV2 && msg == cbr.ErrDupRedeem.Error() { - data := &blindedCredVrfResult{ID: cred.TokenPreimage, Duplicate: true} - - return handlers.RenderContent(ctx, data, w, http.StatusOK) - } - - // Duplicate redemptions are not verified. - if msg == cbr.ErrDupRedeem.Error() || msg == cbr.ErrBadRequest.Error() { - return handlers.WrapError(err, "invalid credentials", http.StatusForbidden) - } - - return handlers.WrapError(err, "Error verifying credentials", http.StatusInternalServerError) - } - - return handlers.RenderContent(ctx, &blindedCredVrfResult{ID: cred.TokenPreimage}, w, http.StatusOK) -} - func createOrderItems(req *model.CreateOrderRequestNew) ([]model.OrderItem, error) { result := make([]model.OrderItem, 0) @@ -1883,14 +1882,3 @@ func durationFromISO(v string) (time.Duration, error) { return time.Until(*durt), nil } - -type blindedCredVrfResult struct { - ID string `json:"id"` - Duplicate bool `json:"duplicate"` -} - -type tlv1CredPresentation struct { - Token string `json:"token"` - IssuedAt string `json:"issuedAt"` - ExpiresAt string `json:"expiresAt"` -} From 2464609a56c01ef49a205114afcb74727de3508a Mon Sep 17 00:00:00 2001 From: clD11 <23483715+clD11@users.noreply.github.com> Date: Thu, 12 Oct 2023 17:19:29 +0100 Subject: [PATCH 4/5] Revert "Revert "Production 2023-11-10_01"" This reverts commit 252406a4eeaafd33302500dc8411a33330559120. --- services/skus/controllers.go | 38 +++--- services/skus/service.go | 232 ++++++++++++++++++----------------- 2 files changed, 144 insertions(+), 126 deletions(-) diff --git a/services/skus/controllers.go b/services/skus/controllers.go index 8dc8750ca..93acffd41 100644 --- a/services/skus/controllers.go +++ b/services/skus/controllers.go @@ -841,48 +841,54 @@ func MerchantTransactions(service *Service) handlers.AppHandler { // VerifyCredentialV2 - version 2 of verify credential func VerifyCredentialV2(service *Service) handlers.AppHandler { - return handlers.AppHandler(func(w http.ResponseWriter, r *http.Request) *handlers.AppError { + return func(w http.ResponseWriter, r *http.Request) *handlers.AppError { + ctx := r.Context() - logger := logging.Logger(ctx, "VerifyCredentialV2") - logger.Debug().Msg("starting VerifyCredentialV2 controller") + l := logging.Logger(ctx, "VerifyCredentialV2") var req = new(VerifyCredentialRequestV2) if err := inputs.DecodeAndValidateReader(ctx, req, r.Body); err != nil { - logger.Error().Err(err).Msg("failed to read request") + l.Error().Err(err).Msg("failed to read request") return handlers.WrapError(err, "Error in request body", http.StatusBadRequest) } - return service.verifyCredential(ctx, req, w) - }) + appErr := service.verifyCredential(ctx, req, w) + if appErr != nil { + l.Error().Err(appErr).Msg("failed to verify credential") + } + + return appErr + } } // VerifyCredentialV1 is the handler for verifying subscription credentials func VerifyCredentialV1(service *Service) handlers.AppHandler { - return handlers.AppHandler(func(w http.ResponseWriter, r *http.Request) *handlers.AppError { + return func(w http.ResponseWriter, r *http.Request) *handlers.AppError { ctx := r.Context() - - logger := logging.Logger(r.Context(), "VerifyCredentialV1") - logger.Debug().Msg("starting VerifyCredentialV1 controller") + l := logging.Logger(r.Context(), "VerifyCredentialV1") var req = new(VerifyCredentialRequestV1) err := requestutils.ReadJSON(r.Context(), r.Body, &req) if err != nil { - logger.Error().Err(err).Msg("failed to read request") + l.Error().Err(err).Msg("failed to read request") return handlers.WrapError(err, "Error in request body", http.StatusBadRequest) } - logger.Debug().Msg("read verify credential post body") + l.Debug().Msg("read verify credential post body") _, err = govalidator.ValidateStruct(req) if err != nil { - logger.Error().Err(err).Msg("failed to validate request") + l.Error().Err(err).Msg("failed to validate request") return handlers.WrapError(err, "Error in request validation", http.StatusBadRequest) } - logger.Debug().Msg("validated verify credential post body") + appErr := service.verifyCredential(ctx, req, w) + if appErr != nil { + l.Error().Err(appErr).Msg("failed to verify credential") + } - return service.verifyCredential(ctx, req, w) - }) + return appErr + } } // WebhookRouter - handles calls from various payment method webhooks informing payments of completion diff --git a/services/skus/service.go b/services/skus/service.go index d2f54b112..0504b49b0 100644 --- a/services/skus/service.go +++ b/services/skus/service.go @@ -1297,9 +1297,8 @@ type credential interface { GetPresentation(context.Context) string } -// TODO refactor this see issue #1502 // verifyCredential - given a credential, verify it. -func (s *Service) verifyCredential(ctx context.Context, req credential, w http.ResponseWriter) *handlers.AppError { +func (s *Service) verifyCredential(ctx context.Context, cred credential, w http.ResponseWriter) *handlers.AppError { logger := logging.Logger(ctx, "verifyCredential") merchant, err := GetMerchant(ctx) @@ -1312,9 +1311,9 @@ func (s *Service) verifyCredential(ctx context.Context, req credential, w http.R caveats := GetCaveats(ctx) - if req.GetMerchantID(ctx) != merchant { + if cred.GetMerchantID(ctx) != merchant { logger.Warn(). - Str("req.MerchantID", req.GetMerchantID(ctx)). + Str("req.MerchantID", cred.GetMerchantID(ctx)). Str("merchant", merchant). Msg("merchant does not match the key's merchant") return handlers.WrapError(nil, "Verify request merchant does not match authentication", http.StatusForbidden) @@ -1324,9 +1323,9 @@ func (s *Service) verifyCredential(ctx context.Context, req credential, w http.R if caveats != nil { if sku, ok := caveats["sku"]; ok { - if req.GetSku(ctx) != sku { + if cred.GetSku(ctx) != sku { logger.Warn(). - Str("req.SKU", req.GetSku(ctx)). + Str("req.SKU", cred.GetSku(ctx)). Str("sku", sku). Msg("sku caveat does not match") return handlers.WrapError(nil, "Verify request sku does not match authentication", http.StatusForbidden) @@ -1335,130 +1334,97 @@ func (s *Service) verifyCredential(ctx context.Context, req credential, w http.R } logger.Debug().Msg("caveats validated") - if req.GetType(ctx) == singleUse || req.GetType(ctx) == timeLimitedV2 { - var bytes []byte - bytes, err = base64.StdEncoding.DecodeString(req.GetPresentation(ctx)) - if err != nil { - return handlers.WrapError(err, "Error in decoding presentation", http.StatusBadRequest) - } + kind := cred.GetType(ctx) + switch kind { + case singleUse, timeLimitedV2: + return s.verifyBlindedTokenCredential(ctx, cred, w) + case timeLimited: + return s.verifyTimeLimitedV1Credential(ctx, cred, w) + default: + return handlers.WrapError(nil, "Unknown credential type", http.StatusBadRequest) + } +} - var decodedCredential cbr.CredentialRedemption - err = json.Unmarshal(bytes, &decodedCredential) - if err != nil { - return handlers.WrapError(err, "Error in presentation formatting", http.StatusBadRequest) - } +// verifyBlindedTokenCredential verifies a single use or time limited v2 credential. +func (s *Service) verifyBlindedTokenCredential(ctx context.Context, req credential, w http.ResponseWriter) *handlers.AppError { + bytes, err := base64.StdEncoding.DecodeString(req.GetPresentation(ctx)) + if err != nil { + return handlers.WrapError(err, "Error in decoding presentation", http.StatusBadRequest) + } - // Ensure that the credential being redeemed (opaque to merchant) matches the outer credential details - issuerID, err := encodeIssuerID(req.GetMerchantID(ctx), req.GetSku(ctx)) - if err != nil { - return handlers.WrapError(err, "Error in outer merchantId or sku", http.StatusBadRequest) - } - if issuerID != decodedCredential.Issuer { - return handlers.WrapError(nil, "Error, outer merchant and sku don't match issuer", http.StatusBadRequest) - } + decodedCred := &cbr.CredentialRedemption{} + if err := json.Unmarshal(bytes, decodedCred); err != nil { + return handlers.WrapError(err, "Error in presentation formatting", http.StatusBadRequest) + } - switch req.GetType(ctx) { - case singleUse: - err = s.cbClient.RedeemCredential(ctx, decodedCredential.Issuer, decodedCredential.TokenPreimage, - decodedCredential.Signature, decodedCredential.Issuer) - case timeLimitedV2: - err = s.cbClient.RedeemCredentialV3(ctx, decodedCredential.Issuer, decodedCredential.TokenPreimage, - decodedCredential.Signature, decodedCredential.Issuer) - default: - return handlers.WrapError(fmt.Errorf("credential type %s not suppoted", req.GetType(ctx)), - "unknown credential type %s", http.StatusBadRequest) - } + // Ensure that the credential being redeemed (opaque to merchant) matches the outer credential details. + issuerID, err := encodeIssuerID(req.GetMerchantID(ctx), req.GetSku(ctx)) + if err != nil { + return handlers.WrapError(err, "Error in outer merchantId or sku", http.StatusBadRequest) + } - if err != nil { - // if this is a duplicate redemption these are not verified - if err.Error() == cbr.ErrDupRedeem.Error() || err.Error() == cbr.ErrBadRequest.Error() { - return handlers.WrapError(err, "invalid credentials", http.StatusForbidden) - } - return handlers.WrapError(err, "Error verifying credentials", http.StatusInternalServerError) - } + if issuerID != decodedCred.Issuer { + return handlers.WrapError(nil, "Error, outer merchant and sku don't match issuer", http.StatusBadRequest) + } + + return s.redeemBlindedCred(ctx, w, req.GetType(ctx), decodedCred) +} + +// verifyTimeLimitedV1Credential verifies a time limited v1 credential. +func (s *Service) verifyTimeLimitedV1Credential(ctx context.Context, req credential, w http.ResponseWriter) *handlers.AppError { + data, err := base64.StdEncoding.DecodeString(req.GetPresentation(ctx)) + if err != nil { + return handlers.WrapError(err, "Error in decoding presentation", http.StatusBadRequest) + } - return handlers.RenderContent(ctx, "Credentials successfully verified", w, http.StatusOK) + present := &tlv1CredPresentation{} + if err := json.Unmarshal(data, present); err != nil { + return handlers.WrapError(err, "Error in presentation formatting", http.StatusBadRequest) } - if req.GetType(ctx) == "time-limited" { - // Presentation includes a token and token metadata test test - type Presentation struct { - IssuedAt string `json:"issuedAt"` - ExpiresAt string `json:"expiresAt"` - Token string `json:"token"` - } + merchID := req.GetMerchantID(ctx) - var bytes []byte - bytes, err = base64.StdEncoding.DecodeString(req.GetPresentation(ctx)) - if err != nil { - logger.Error().Err(err). - Msg("failed to decode the request token presentation") - return handlers.WrapError(err, "Error in decoding presentation", http.StatusBadRequest) - } - logger.Debug().Str("presentation", string(bytes)).Msg("presentation decoded") + // Ensure that the credential being redeemed (opaque to merchant) matches the outer credential details. + issuerID, err := encodeIssuerID(merchID, req.GetSku(ctx)) + if err != nil { + return handlers.WrapError(err, "Error in outer merchantId or sku", http.StatusBadRequest) + } - var presentation Presentation - err = json.Unmarshal(bytes, &presentation) - if err != nil { - logger.Error().Err(err). - Msg("failed to unmarshal the request token presentation") - return handlers.WrapError(err, "Error in presentation formatting", http.StatusBadRequest) - } + keys, err := s.GetCredentialSigningKeys(ctx, merchID) + if err != nil { + return handlers.WrapError(err, "failed to get merchant signing key", http.StatusInternalServerError) + } - logger.Debug().Str("presentation", string(bytes)).Msg("presentation unmarshalled") + issuedAt, err := time.Parse("2006-01-02", present.IssuedAt) + if err != nil { + return handlers.WrapError(err, "Error parsing issuedAt", http.StatusBadRequest) + } - // Ensure that the credential being redeemed (opaque to merchant) matches the outer credential details - issuerID, err := encodeIssuerID(req.GetMerchantID(ctx), req.GetSku(ctx)) - if err != nil { - logger.Error().Err(err). - Msg("failed to encode the issuer id") - return handlers.WrapError(err, "Error in outer merchantId or sku", http.StatusBadRequest) - } - logger.Debug().Str("issuer", issuerID).Msg("issuer encoded") + expiresAt, err := time.Parse("2006-01-02", present.ExpiresAt) + if err != nil { + return handlers.WrapError(err, "Error parsing expiresAt", http.StatusBadRequest) + } - keys, err := s.GetCredentialSigningKeys(ctx, req.GetMerchantID(ctx)) - if err != nil { - return handlers.WrapError(err, "failed to get merchant signing key", http.StatusInternalServerError) - } + for _, key := range keys { + timeLimitedSecret := cryptography.NewTimeLimitedSecret(key) - issuedAt, err := time.Parse("2006-01-02", presentation.IssuedAt) - if err != nil { - logger.Error().Err(err). - Msg("failed to parse issued at time of credential") - return handlers.WrapError(err, "Error parsing issuedAt", http.StatusBadRequest) - } - expiresAt, err := time.Parse("2006-01-02", presentation.ExpiresAt) + verified, err := timeLimitedSecret.Verify([]byte(issuerID), issuedAt, expiresAt, present.Token) if err != nil { - logger.Error().Err(err). - Msg("failed to parse expires at time of credential") - return handlers.WrapError(err, "Error parsing expiresAt", http.StatusBadRequest) + return handlers.WrapError(err, "Error in token verification", http.StatusBadRequest) } - for _, key := range keys { - timeLimitedSecret := cryptography.NewTimeLimitedSecret(key) - verified, err := timeLimitedSecret.Verify([]byte(issuerID), issuedAt, expiresAt, presentation.Token) - if err != nil { - logger.Error().Err(err). - Msg("failed to verify time limited credential") - return handlers.WrapError(err, "Error in token verification", http.StatusBadRequest) + if verified { + // Check against expiration time, issued time. + now := time.Now() + if now.After(expiresAt) || now.Before(issuedAt) { + return handlers.WrapError(nil, "Credentials are not valid", http.StatusForbidden) } - if verified { - // check against expiration time, issued time - if time.Now().After(expiresAt) || time.Now().Before(issuedAt) { - logger.Error(). - Msg("credentials are not valid") - return handlers.RenderContent(ctx, "Credentials are not valid", w, http.StatusForbidden) - } - logger.Debug().Msg("credentials verified") - return handlers.RenderContent(ctx, "Credentials successfully verified", w, http.StatusOK) - } + return handlers.RenderContent(ctx, "Credentials successfully verified", w, http.StatusOK) } - logger.Error(). - Msg("credentials could not be verified") - return handlers.RenderContent(ctx, "Credentials could not be verified", w, http.StatusForbidden) } - return handlers.WrapError(nil, "Unknown credential type", http.StatusBadRequest) + + return handlers.WrapError(nil, "Credentials could not be verified", http.StatusForbidden) } // RunSendSigningRequestJob - send the order credentials signing requests @@ -1806,6 +1772,41 @@ func (s *Service) createStripeSessID(ctx context.Context, req *model.CreateOrder return nil } +func (s *Service) redeemBlindedCred(ctx context.Context, w http.ResponseWriter, kind string, cred *cbr.CredentialRedemption) *handlers.AppError { + var redeemFn func(ctx context.Context, issuer, preimage, signature, payload string) error + + switch kind { + case singleUse: + redeemFn = s.cbClient.RedeemCredential + case timeLimitedV2: + redeemFn = s.cbClient.RedeemCredentialV3 + default: + return handlers.WrapError(fmt.Errorf("credential type %s not suppoted", kind), "unknown credential type %s", http.StatusBadRequest) + } + + // FIXME: we shouldn't be using the issuer as the payload, it ideally would be a unique request identifier + // to allow for more flexible idempotent behavior. + if err := redeemFn(ctx, cred.Issuer, cred.TokenPreimage, cred.Signature, cred.Issuer); err != nil { + msg := err.Error() + + // Time limited v2: Expose a credential id so the caller can decide whether to allow multiple redemptions. + if kind == timeLimitedV2 && msg == cbr.ErrDupRedeem.Error() { + data := &blindedCredVrfResult{ID: cred.TokenPreimage, Duplicate: true} + + return handlers.RenderContent(ctx, data, w, http.StatusOK) + } + + // Duplicate redemptions are not verified. + if msg == cbr.ErrDupRedeem.Error() || msg == cbr.ErrBadRequest.Error() { + return handlers.WrapError(err, "invalid credentials", http.StatusForbidden) + } + + return handlers.WrapError(err, "Error verifying credentials", http.StatusInternalServerError) + } + + return handlers.RenderContent(ctx, &blindedCredVrfResult{ID: cred.TokenPreimage}, w, http.StatusOK) +} + func createOrderItems(req *model.CreateOrderRequestNew) ([]model.OrderItem, error) { result := make([]model.OrderItem, 0) @@ -1882,3 +1883,14 @@ func durationFromISO(v string) (time.Duration, error) { return time.Until(*durt), nil } + +type blindedCredVrfResult struct { + ID string `json:"id"` + Duplicate bool `json:"duplicate"` +} + +type tlv1CredPresentation struct { + Token string `json:"token"` + IssuedAt string `json:"issuedAt"` + ExpiresAt string `json:"expiresAt"` +} From 98d424c8b12e44e77b736a7bd9025e7810084a0a Mon Sep 17 00:00:00 2001 From: clD11 <23483715+clD11@users.noreply.github.com> Date: Thu, 12 Oct 2023 17:51:33 +0100 Subject: [PATCH 5/5] fix: multi redeem tlv2 success response change tlv2 response --- services/skus/service.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/skus/service.go b/services/skus/service.go index 0504b49b0..c8c727063 100644 --- a/services/skus/service.go +++ b/services/skus/service.go @@ -1804,7 +1804,11 @@ func (s *Service) redeemBlindedCred(ctx context.Context, w http.ResponseWriter, return handlers.WrapError(err, "Error verifying credentials", http.StatusInternalServerError) } - return handlers.RenderContent(ctx, &blindedCredVrfResult{ID: cred.TokenPreimage}, w, http.StatusOK) + // TODO(clD11): cleanup after quick fix + if kind == timeLimitedV2 { + return handlers.RenderContent(ctx, &blindedCredVrfResult{ID: cred.TokenPreimage}, w, http.StatusOK) + } + return handlers.RenderContent(ctx, "Credentials successfully verified", w, http.StatusOK) } func createOrderItems(req *model.CreateOrderRequestNew) ([]model.OrderItem, error) {