diff --git a/services/wallet/controllers_v3_test.go b/services/wallet/controllers_v3_test.go index d1ea2e629..890d583fc 100644 --- a/services/wallet/controllers_v3_test.go +++ b/services/wallet/controllers_v3_test.go @@ -16,7 +16,7 @@ import ( "testing" "time" - sqlmock "github.com/DATA-DOG/go-sqlmock" + "github.com/DATA-DOG/go-sqlmock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -34,7 +34,7 @@ import ( "github.com/golang/mock/gomock" "github.com/jmoiron/sqlx" uuid "github.com/satori/go.uuid" - jose "gopkg.in/square/go-jose.v2" + "gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2/jwt" ) @@ -259,6 +259,16 @@ func TestLinkBitFlyerWalletV3(t *testing.T) { ctx = context.WithValue(ctx, appctx.ReputationClientCTXKey, mockReputation) ctx = context.WithValue(ctx, appctx.NoUnlinkPriorToDurationCTXKey, "-P1D") + mockReputation.EXPECT().IsLinkingReputable( + gomock.Any(), // ctx + gomock.Any(), // wallet id + gomock.Any(), // country + ).Return( + true, + []int{}, + nil, + ) + r = r.WithContext(ctx) router := chi.NewRouter() @@ -317,6 +327,16 @@ func TestLinkGeminiWalletV3RelinkBadRegion(t *testing.T) { rw = httptest.NewRecorder() ) + mockReputationClient.EXPECT().IsLinkingReputable( + gomock.Any(), // ctx + gomock.Any(), // wallet id + gomock.Any(), // country + ).Return( + true, + []int{}, + nil, + ) + ctx = context.WithValue(ctx, appctx.DatastoreCTXKey, datastore) ctx = context.WithValue(ctx, appctx.ReputationClientCTXKey, mockReputationClient) ctx = context.WithValue(ctx, appctx.GeminiClientCTXKey, mockGeminiClient) @@ -537,6 +557,16 @@ func TestLinkGeminiWalletV3FirstLinking(t *testing.T) { rw = httptest.NewRecorder() ) + mockReputationClient.EXPECT().IsLinkingReputable( + gomock.Any(), // ctx + gomock.Any(), // wallet id + gomock.Any(), // country + ).Return( + true, + []int{}, + nil, + ) + ctx = context.WithValue(ctx, appctx.DatastoreCTXKey, datastore) ctx = context.WithValue(ctx, appctx.ReputationClientCTXKey, mockReputationClient) ctx = context.WithValue(ctx, appctx.GeminiClientCTXKey, mockGeminiClient) @@ -740,6 +770,16 @@ func TestLinkZebPayWalletV3(t *testing.T) { )), ) + mockReputationClient.EXPECT().IsLinkingReputable( + gomock.Any(), // ctx + gomock.Any(), // wallet id + gomock.Any(), // country + ).Return( + true, + []int{}, + nil, + ) + mockSQLCustodianLink(mock, "zebpay") // begin linking tx @@ -847,6 +887,16 @@ func TestLinkGeminiWalletV3(t *testing.T) { nil, ) + mockReputationClient.EXPECT().IsLinkingReputable( + gomock.Any(), // ctx + gomock.Any(), // wallet id + gomock.Any(), // country + ).Return( + true, + []int{}, + nil, + ) + mockSQLCustodianLink(mock, "gemini") // begin linking tx diff --git a/services/wallet/datastore.go b/services/wallet/datastore.go index 4b545c233..7e6d68ba6 100644 --- a/services/wallet/datastore.go +++ b/services/wallet/datastore.go @@ -71,7 +71,7 @@ func init() { // Datastore holds the interface for the wallet datastore type Datastore interface { datastore.Datastore - LinkWallet(ctx context.Context, ID string, providerID string, providerLinkingID uuid.UUID, depositProvider string) error + LinkWallet(ctx context.Context, ID string, providerID string, providerLinkingID uuid.UUID, anonymousAddress *uuid.UUID, depositProvider, country string) error GetLinkingLimitInfo(ctx context.Context, providerLinkingID string) (map[string]LinkingInfo, error) HasPriorLinking(ctx context.Context, walletID uuid.UUID, providerLinkingID uuid.UUID) (bool, error) // GetLinkingsByProviderLinkingID gets the wallet linking info by provider linking id @@ -561,10 +561,47 @@ var ( ) // LinkWallet links a wallet together -func (pg *Postgres) LinkWallet(ctx context.Context, ID string, userDepositDestination string, providerLinkingID uuid.UUID, depositProvider string) error { +func (pg *Postgres) LinkWallet(ctx context.Context, ID string, userDepositDestination string, providerLinkingID uuid.UUID, anonymousAddress *uuid.UUID, depositProvider, country string) error { sublogger := logger(ctx).With().Str("wallet_id", ID).Logger() sublogger.Debug().Msg("linking wallet") + // rep check + if repClient, ok := ctx.Value(appctx.ReputationClientCTXKey).(reputation.Client); ok { + walletID, err := uuid.FromString(ID) + if err != nil { + sublogger.Warn().Err(err).Msg("invalid wallet id") + return fmt.Errorf("invalid wallet id, not uuid: %w", err) + } + // we have a client, check the value for ID + reputable, cohorts, err := repClient.IsLinkingReputable(ctx, walletID, country) + if err != nil { + sublogger.Warn().Err(err).Msg("failed to check reputation") + return fmt.Errorf("failed to check wallet rep: %w", err) + } + + var ( + isTooYoung = false + geoResetDifferent = false + ) + for _, v := range cohorts { + if isTooYoung = (v == reputation.CohortTooYoung); isTooYoung { + break + } + if geoResetDifferent = (v == reputation.CohortGeoResetDifferent); geoResetDifferent { + break + } + } + + if !reputable && !isTooYoung && !geoResetDifferent { + sublogger.Info().Msg("wallet linking attempt failed - unusual activity") + countLinkingFlaggedUnusual.Inc() + return ErrUnusualActivity + } else if geoResetDifferent { + sublogger.Info().Msg("wallet linking attempt failed - geo reset is different") + return ErrGeoResetDifferent + } + } + ctx, tx, rollback, commit, err := getTx(ctx, pg) if err != nil { sublogger.Error().Err(err).Msg("error getting tx") diff --git a/services/wallet/datastore_test.go b/services/wallet/datastore_test.go index 54b126369..6370a8560 100644 --- a/services/wallet/datastore_test.go +++ b/services/wallet/datastore_test.go @@ -226,7 +226,7 @@ func (suite *WalletPostgresTestSuite) TestLinkWallet_Concurrent_InsertUpdate() { go func() { defer wg.Done() err = pg.LinkWallet(context.WithValue(context.Background(), appctx.NoUnlinkPriorToDurationCTXKey, "-P1D"), - walletInfo.ID, userDepositDestination, providerLinkingID, walletInfo.Provider) + walletInfo.ID, userDepositDestination, providerLinkingID, walletInfo.AnonymousAddress, walletInfo.Provider, "") }() } @@ -260,7 +260,7 @@ func (suite *WalletPostgresTestSuite) seedWallet(pg Datastore) (string, uuid.UUI suite.Require().NoError(err, "save wallet should succeed") err = pg.LinkWallet(context.WithValue(context.Background(), appctx.NoUnlinkPriorToDurationCTXKey, "-P1D"), - walletInfo.ID, userDepositDestination, providerLinkingID, "uphold") + walletInfo.ID, userDepositDestination, providerLinkingID, walletInfo.AnonymousAddress, "uphold", "") suite.Require().NoError(err, "link wallet should succeed") } @@ -303,7 +303,7 @@ func (suite *WalletPostgresTestSuite) TestLinkWallet_Concurrent_MaxLinkCount() { go func(index int) { defer wg.Done() err = pg.LinkWallet(context.WithValue(context.Background(), appctx.NoUnlinkPriorToDurationCTXKey, "-P1D"), - wallets[index].ID, userDepositDestination, providerLinkingID, wallets[index].Provider) + wallets[index].ID, userDepositDestination, providerLinkingID, wallets[index].AnonymousAddress, wallets[index].Provider, "") }(i) } diff --git a/services/wallet/instrumented_datastore.go b/services/wallet/instrumented_datastore.go index a5c4c7b9c..837783d57 100644 --- a/services/wallet/instrumented_datastore.go +++ b/services/wallet/instrumented_datastore.go @@ -1,9 +1,9 @@ -package wallet - // Code generated by gowrap. DO NOT EDIT. // template: ../../.prom-gowrap.tmpl // gowrap: http://github.com/hexdigest/gowrap +package wallet + //go:generate gowrap gen -p github.com/brave-intl/bat-go/services/wallet -i Datastore -t ../../.prom-gowrap.tmpl -o instrumented_datastore.go -l "" import ( @@ -255,7 +255,7 @@ func (_d DatastoreWithPrometheus) InsertWalletTx(ctx context.Context, tx *sqlx.T } // LinkWallet implements Datastore -func (_d DatastoreWithPrometheus) LinkWallet(ctx context.Context, ID string, providerID string, providerLinkingID uuid.UUID, depositProvider string) (err error) { +func (_d DatastoreWithPrometheus) LinkWallet(ctx context.Context, ID string, providerID string, providerLinkingID uuid.UUID, anonymousAddress *uuid.UUID, depositProvider string, country string) (err error) { _since := time.Now() defer func() { result := "ok" @@ -265,7 +265,7 @@ func (_d DatastoreWithPrometheus) LinkWallet(ctx context.Context, ID string, pro datastoreDurationSummaryVec.WithLabelValues(_d.instanceName, "LinkWallet", result).Observe(time.Since(_since).Seconds()) }() - return _d.base.LinkWallet(ctx, ID, providerID, providerLinkingID, depositProvider) + return _d.base.LinkWallet(ctx, ID, providerID, providerLinkingID, anonymousAddress, depositProvider, country) } // Migrate implements Datastore diff --git a/services/wallet/service.go b/services/wallet/service.go index 900b295dc..90819c180 100644 --- a/services/wallet/service.go +++ b/services/wallet/service.go @@ -415,7 +415,7 @@ func (service *Service) LinkBitFlyerWallet(ctx context.Context, walletID uuid.UU // we also validated that this "info" signed the request to perform the linking with http signature // we assume that since we got linkingInfo signed from BF that they are KYC providerLinkingID := uuid.NewV5(ClaimNamespace, accountHash) - err = service.Datastore.LinkWallet(ctx, walletID.String(), depositID, providerLinkingID, depositProvider) + err = service.Datastore.LinkWallet(ctx, walletID.String(), depositID, providerLinkingID, nil, depositProvider, country) if err != nil { if errors.Is(err, ErrUnusualActivity) { return "", handlers.WrapError(err, "unable to link - unusual activity", http.StatusBadRequest) @@ -497,7 +497,7 @@ func (service *Service) LinkZebPayWallet(ctx context.Context, walletID uuid.UUID } providerLinkingID := uuid.NewV5(ClaimNamespace, claims.AccountID) - if err := service.Datastore.LinkWallet(ctx, walletID.String(), claims.DepositID, providerLinkingID, depositProvider); err != nil { + if err := service.Datastore.LinkWallet(ctx, walletID.String(), claims.DepositID, providerLinkingID, nil, depositProvider, country); err != nil { if errors.Is(err, ErrUnusualActivity) { return "", handlers.WrapError(err, "unable to link - unusual activity", http.StatusBadRequest) } @@ -567,7 +567,7 @@ func (service *Service) LinkGeminiWallet(ctx context.Context, walletID uuid.UUID // we assume that since we got linking_info(VerificationToken) signed from Gemini that they are KYC providerLinkingID := uuid.NewV5(ClaimNamespace, accountID) - err = service.Datastore.LinkWallet(ctx, walletID.String(), depositID, providerLinkingID, depositProvider) + err = service.Datastore.LinkWallet(ctx, walletID.String(), depositID, providerLinkingID, nil, depositProvider, country) if err != nil { if errors.Is(err, ErrUnusualActivity) { return "", handlers.WrapError(err, "unable to link - unusual activity", http.StatusBadRequest) @@ -589,7 +589,7 @@ func (service *Service) LinkGeminiWallet(ctx context.Context, walletID uuid.UUID } // LinkUpholdWallet links an uphold.Wallet and transfers funds. -func (service *Service) LinkUpholdWallet(ctx context.Context, wallet uphold.Wallet, transaction string, _ *uuid.UUID) (string, error) { +func (service *Service) LinkUpholdWallet(ctx context.Context, wallet uphold.Wallet, transaction string, anonymousAddress *uuid.UUID) (string, error) { const depositProvider = "uphold" // do not confirm this transaction yet info := wallet.GetWalletInfo() @@ -669,7 +669,7 @@ func (service *Service) LinkUpholdWallet(ctx context.Context, wallet uphold.Wall providerLinkingID := uuid.NewV5(ClaimNamespace, userID) // tx.Destination will be stored as UserDepositDestination in the wallet info upon linking - err = service.Datastore.LinkWallet(ctx, info.ID, transactionInfo.Destination, providerLinkingID, depositProvider) + err = service.Datastore.LinkWallet(ctx, info.ID, transactionInfo.Destination, providerLinkingID, anonymousAddress, depositProvider, country) if err != nil { if errors.Is(err, ErrUnusualActivity) { return "", handlers.WrapError(err, "unable to link - unusual activity", http.StatusBadRequest)