From e0c4a8001bd3c3e28f2201f6907bc0c3153e3008 Mon Sep 17 00:00:00 2001 From: Ocasta Date: Wed, 26 Jul 2023 15:41:29 -0500 Subject: [PATCH 01/20] first pass --- api/config.go | 3 ++ api/handler/user.go | 26 +++++++++++ pkg/model/entity.go | 14 ++++++ pkg/model/request.go | 10 +++++ pkg/repository/identity.go | 85 ++++++++++++++++++++++++++++++++++++ pkg/repository/repository.go | 1 + pkg/service/user.go | 48 +++++++++++++++++++- 7 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 pkg/repository/identity.go diff --git a/api/config.go b/api/config.go index 3b0f456a..c7d3d6c0 100644 --- a/api/config.go +++ b/api/config.go @@ -23,6 +23,7 @@ func NewRepos(config APIConfig) repository.Repositories { TxLeg: repository.NewTxLeg(config.DB), Location: repository.NewLocation(config.DB), Platform: repository.NewPlatform(config.DB), + Identity: repository.NewIdentity(config.DB), } } @@ -49,6 +50,7 @@ func NewServices(config APIConfig, repos repository.Repositories) service.Servic transaction := service.NewTransaction(repos, config.Redis, unit21) user := service.NewUser(repos, auth, fingerprint, device, unit21, verification) + identity := service.NewIdentity(repos) card := service.NewCard(repos) @@ -59,6 +61,7 @@ func NewServices(config APIConfig, repos repository.Repositories) service.Servic Geofencing: geofencing, Transaction: transaction, User: user, + Identity: identity, Verification: verification, Device: device, Card: card, diff --git a/api/handler/user.go b/api/handler/user.go index 01c5d0ed..f5590586 100644 --- a/api/handler/user.go +++ b/api/handler/user.go @@ -21,6 +21,7 @@ type User interface { PreviewEmail(c echo.Context) error VerifyEmail(c echo.Context) error PreValidateEmail(c echo.Context) error + GetPersonaAccountId(c echo.Context) error RegisterRoutes(g *echo.Group, ms ...echo.MiddlewareFunc) RegisterPrivateRoutes(g *echo.Group, ms ...echo.MiddlewareFunc) } @@ -367,6 +368,30 @@ func (u user) PreValidateEmail(c echo.Context) error { return c.JSON(http.StatusOK, ResultMessage{Status: "validated"}) } +// @Summary Get user persona account id +// @Description Get user persona account id +// @Tags Users +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param id path string true "User ID" +// @Success 200 {object} string +// @Failure 400 {object} error +// @Failure 401 {object} error +// @Failure 500 {object} error +// @Router /users/persona-account-id [get] +func (u user) GetPersonaAccountId(c echo.Context) error { + ctx := c.Request().Context() + + userId := c.Get("userId") + + identity, err := u.userService.GetPersonaAccountId(ctx, userId) + if err != nil { + return httperror.Internal500(c) + } + return c.JSON(http.StatusOK, identity.Data.Id) +} + // @Summary Get user email preview // @Description Get obscured user email // @Tags Users @@ -428,6 +453,7 @@ func (u user) RegisterRoutes(g *echo.Group, ms ...echo.MiddlewareFunc) { g.POST("/preview-email", u.PreviewEmail, ms[0]) g.POST("/verify-device", u.RequestDeviceVerification, ms[0]) g.POST("/device-status", u.GetDeviceStatus, ms[0]) + // the rest of the endpoints use the JWT auth and do not require an API Key // hence removing the first (API key) middleware ms = ms[1:] diff --git a/pkg/model/entity.go b/pkg/model/entity.go index 41a9bb73..c1a66ff1 100644 --- a/pkg/model/entity.go +++ b/pkg/model/entity.go @@ -24,6 +24,20 @@ type User struct { Email string `json:"email"` } +type Identity struct { + Id string `json:"id,omitempty" db:"id"` + Level string `json:"level" db:"level"` + AccountId string `json:"accountId" db:"account_id"` + UserId string `json:"userId" db:"user_id"` + CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt,omitempty" db:"updated_at"` + DeletedAt *time.Time `json:"deletedAt,omitempty" db:"deleted_at"` + EmailVerified *time.Time `json:"emailVerified,omitempty" db:"email_verified"` + PhoneVerified *time.Time `json:"phoneVerified,omitempty" db:"phone_verified"` + SelfieVerified *time.Time `json:"selfieVerified,omitempty" db:"selfie_verified"` + DocumentVerified *time.Time `json:"documentVerified,omitempty" db:"document_verified"` +} + type UserWithContact struct { User Email string `db:"email"` diff --git a/pkg/model/request.go b/pkg/model/request.go index 0154963d..761dc1e0 100644 --- a/pkg/model/request.go +++ b/pkg/model/request.go @@ -77,6 +77,16 @@ type UserUpdates struct { LastName *string `json:"lastName" db:"last_name"` } +type IdentityUpdates struct { + Level *string `json:"level" db:"level"` + AccountId *string `json:"accountId" db:"account_id"` + UserId *string `json:"userId" db:"user_id"` + EmailVerified *time.Time `json:"emailVerified,omitempty" db:"email_verified"` + PhoneVerified *time.Time `json:"phoneVerified,omitempty" db:"phone_verified"` + SelfieVerified *time.Time `json:"selfieVerified,omitempty" db:"selfie_verified"` + DocumentVerified *time.Time `json:"documentVerified,omitempty" db:"document_verified"` +} + type UserPKLogin struct { PublicAddress string `json:"publicAddress"` Signature string `json:"signature"` diff --git a/pkg/repository/identity.go b/pkg/repository/identity.go new file mode 100644 index 00000000..0e1b2502 --- /dev/null +++ b/pkg/repository/identity.go @@ -0,0 +1,85 @@ +package repository + +import ( + "context" + "errors" + "fmt" + "strings" + + libcommon "github.com/String-xyz/go-lib/v2/common" + "github.com/String-xyz/go-lib/v2/database" + baserepo "github.com/String-xyz/go-lib/v2/repository" + "github.com/String-xyz/string-api/pkg/model" +) + +type Identity interface { + Create(ctx context.Context, insert model.Identity) (identity model.Identity, err error) + GetById(ctx context.Context, id string) (model.Identity, error) + List(ctx context.Context, limit int, offset int) ([]model.Identity, error) + Update(ctx context.Context, id string, updates any) (identity model.Identity, err error) + GetByUserId(ctx context.Context, userId string) (identity model.Identity, err error) + GetByAccountId(ctx context.Context, accountId string) (identity model.Identity, err error) +} + +type identity[T any] struct { + baserepo.Base[T] +} + +func NewIdentity(db database.Queryable) Identity { + return &identity[model.Identity]{baserepo.Base[model.Identity]{Store: db, Table: "identity"}} +} + +func (i identity[T]) Create(ctx context.Context, insert model.Identity) (identity model.Identity, err error) { + query, args, err := i.Named(` + INSERT INTO identity (userId) + VALUES(:userId) RETURNING *`, insert) + if err != nil { + return identity, libcommon.StringError(err) + } + + err = i.Store.QueryRowxContext(ctx, query, args...).StructScan(&m) + if err != nil { + return identity, libcommon.StringError(err) + } + + return identity, nil +} + +func (i identity[T]) Update(ctx context.Context, id string, updates any) (identity model.Identity, err error) { + names, keyToUpdate := libcommon.KeysAndValues(updates) + if len(names) == 0 { + return identity, libcommon.StringError(errors.New("no updates provided")) + } + + keyToUpdate["id"] = id + + query := fmt.Sprintf("UPDATE %s SET %s WHERE id=:id RETURNING *", i.Table, strings.Join(names, ",")) + namedQuery, args, err := i.Named(query, keyToUpdate) + if err != nil { + return identity, libcommon.StringError(err) + } + + err = i.Store.QueryRowxContext(ctx, namedQuery, args...).StructScan(&identity) + if err != nil { + return identity, libcommon.StringError(err) + } + return identity, nil +} + +func (i identity[T]) GetByUserId(ctx context.Context, userId string) (identity model.Identity, err error) { + query := fmt.Sprintf("SELECT * FROM %s WHERE userId=$1", i.Table) + err = i.Store.QueryRowxContext(ctx, query, userId).StructScan(&identity) + if err != nil { + return identity, libcommon.StringError(err) + } + return identity, nil +} + +func (i identity[T]) GetByAccountId(ctx context.Context, accountId string) (identity model.Identity, err error) { + query := fmt.Sprintf("SELECT * FROM %s WHERE accountId=$1", i.Table) + err = i.Store.QueryRowxContext(ctx, query, accountId).StructScan(&identity) + if err != nil { + return identity, libcommon.StringError(err) + } + return identity, nil +} diff --git a/pkg/repository/repository.go b/pkg/repository/repository.go index 7b138f98..ff6a350b 100644 --- a/pkg/repository/repository.go +++ b/pkg/repository/repository.go @@ -14,4 +14,5 @@ type Repositories struct { Transaction Transaction TxLeg TxLeg Location Location + Identity Identity } diff --git a/pkg/service/user.go b/pkg/service/user.go index 23aed82c..cb04037a 100644 --- a/pkg/service/user.go +++ b/pkg/service/user.go @@ -8,7 +8,9 @@ import ( libcommon "github.com/String-xyz/go-lib/v2/common" serror "github.com/String-xyz/go-lib/v2/stringerror" + "github.com/String-xyz/string-api/config" "github.com/String-xyz/string-api/pkg/internal/common" + "github.com/String-xyz/string-api/pkg/internal/persona" "github.com/String-xyz/string-api/pkg/model" "github.com/String-xyz/string-api/pkg/repository" @@ -42,6 +44,8 @@ type User interface { // GetDeviceStatus checks the status of the device verification GetDeviceStatus(ctx context.Context, request model.WalletSignaturePayloadSigned) (model.UserOnboardingStatus, error) + + GetPersonaAccountId(ctx context.Context, userId string) (accountId string, err error) } type user struct { @@ -51,10 +55,12 @@ type user struct { device Device unit21 Unit21 verification Verification + persona persona.PersonaClient } func NewUser(repos repository.Repositories, auth Auth, fprint Fingerprint, device Device, unit21 Unit21, verificationSrv Verification) User { - return &user{repos, auth, fprint, device, unit21, verificationSrv} + persona := persona.New(config.Var.PERSONA_API_KEY) + return &user{repos, auth, fprint, device, unit21, verificationSrv, *persona} } func (u user) GetStatus(ctx context.Context, userId string) (model.UserOnboardingStatus, error) { @@ -353,3 +359,43 @@ func (u user) PreviewEmail(ctx context.Context, request model.WalletSignaturePay return email, nil } + +func (u user) GetPersonaAccountId(ctx context.Context, userId string) (accountId string, err error) { + _, finish := Span(ctx, "service.user.GetPersonaAccountId") + defer finish() + + identity, err := u.repos.Identity.GetByUserId(ctx, userId) + if err != nil { + return accountId, libcommon.StringError(err) + } + if identity.AccountId != "" { + return identity.AccountId, nil + } + + user, err := u.repos.User.GetById(ctx, userId) + if err != nil { + return accountId, libcommon.StringError(err) + } + + request := persona.AccountCreateRequest{ + Data: persona.AccountCreate{ + Attributes: persona.CommonFields{ + EmailAddress: user.Email, + NameFirst: user.FirstName, + NameLast: user.LastName, + NameMiddle: user.MiddleName, + }, + }, + } + account, err := u.persona.CreateAccount(request) + if err != nil { + return accountId, libcommon.StringError(err) + } + + identity, err = u.repos.Identity.Update(ctx, identity.Id, model.IdentityUpdates{AccountId: &account.Data.Id}) + if err != nil { + return accountId, libcommon.StringError(err) + } + + return account.Data.Id, nil +} From 653be586b57441b52cb1db30c10c9faa51127629 Mon Sep 17 00:00:00 2001 From: Ocasta Date: Wed, 26 Jul 2023 15:57:07 -0500 Subject: [PATCH 02/20] fix some bugs, clear unneeded code --- api/config.go | 2 -- api/handler/user.go | 9 ++++++--- pkg/repository/identity.go | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/api/config.go b/api/config.go index c7d3d6c0..cd701b07 100644 --- a/api/config.go +++ b/api/config.go @@ -50,7 +50,6 @@ func NewServices(config APIConfig, repos repository.Repositories) service.Servic transaction := service.NewTransaction(repos, config.Redis, unit21) user := service.NewUser(repos, auth, fingerprint, device, unit21, verification) - identity := service.NewIdentity(repos) card := service.NewCard(repos) @@ -61,7 +60,6 @@ func NewServices(config APIConfig, repos repository.Repositories) service.Servic Geofencing: geofencing, Transaction: transaction, User: user, - Identity: identity, Verification: verification, Device: device, Card: card, diff --git a/api/handler/user.go b/api/handler/user.go index f5590586..d1aecb90 100644 --- a/api/handler/user.go +++ b/api/handler/user.go @@ -383,13 +383,16 @@ func (u user) PreValidateEmail(c echo.Context) error { func (u user) GetPersonaAccountId(c echo.Context) error { ctx := c.Request().Context() - userId := c.Get("userId") + userId, ok := c.Get("userId").(string) + if !ok { + return httperror.Internal500(c, "missing or invalid userId") + } - identity, err := u.userService.GetPersonaAccountId(ctx, userId) + accountId, err := u.userService.GetPersonaAccountId(ctx, userId) if err != nil { return httperror.Internal500(c) } - return c.JSON(http.StatusOK, identity.Data.Id) + return c.JSON(http.StatusOK, accountId) } // @Summary Get user email preview diff --git a/pkg/repository/identity.go b/pkg/repository/identity.go index 0e1b2502..32d676d0 100644 --- a/pkg/repository/identity.go +++ b/pkg/repository/identity.go @@ -37,7 +37,7 @@ func (i identity[T]) Create(ctx context.Context, insert model.Identity) (identit return identity, libcommon.StringError(err) } - err = i.Store.QueryRowxContext(ctx, query, args...).StructScan(&m) + err = i.Store.QueryRowxContext(ctx, query, args...).StructScan(&identity) if err != nil { return identity, libcommon.StringError(err) } From 4aba2fb30bc55834ca02503cf87a52b122d8054f Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 26 Jul 2023 14:34:05 -0700 Subject: [PATCH 03/20] check and identify priviledge, assess true cost, true priviledge level after transaction --- pkg/internal/common/util.go | 9 +++ pkg/model/transaction.go | 1 + pkg/service/executor.go | 26 +++++--- pkg/service/transaction.go | 111 +++++++++++++++++++++++---------- pkg/service/user_priviledge.go | 41 ++++++++++++ 5 files changed, 145 insertions(+), 43 deletions(-) create mode 100644 pkg/service/user_priviledge.go diff --git a/pkg/internal/common/util.go b/pkg/internal/common/util.go index a5df5442..2c955b0b 100644 --- a/pkg/internal/common/util.go +++ b/pkg/internal/common/util.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "math" + "math/big" "strconv" "strings" @@ -82,3 +83,11 @@ func StringContainsAny(target string, substrs []string) bool { } return false } + +func StringifyBigIntArray(arr []*big.Int) string { + strArr := []string{} + for _, elem := range arr { + strArr = append(strArr, elem.String()) + } + return strings.Join(strArr, ",") +} diff --git a/pkg/model/transaction.go b/pkg/model/transaction.go index 17d34554..5de44f30 100644 --- a/pkg/model/transaction.go +++ b/pkg/model/transaction.go @@ -21,6 +21,7 @@ type Quote struct { TransactionRequest TransactionRequest `json:"request" validate:"required"` Estimate Estimate[string] `json:"estimate" validate:"required"` Signature string `json:"signature" validate:"required,base64"` + PriviledgeLevel int `json:"priviledgeLevel" validate:"required"` } type ExecutionRequest struct { diff --git a/pkg/service/executor.go b/pkg/service/executor.go index 1b01212a..ec7b2047 100644 --- a/pkg/service/executor.go +++ b/pkg/service/executor.go @@ -43,8 +43,8 @@ type Executor interface { Close() error GetByChainId() (uint64, error) GetBalance() (float64, error) - GetTokenIds(txIds []string) ([]string, error) - GetTokenQuantities(txIds []string) ([]string, error) + GetTokenIds(txIds []string) ([]string, []string, error) + GetTokenQuantities(txIds []string) ([]string, []*big.Int, error) GetEventData(txIds []string, eventSignature string) ([]types.Log, error) ForwardNonFungibleTokens(txIds []string, recipient string) ([]string, []string, error) ForwardTokens(txIds []string, recipient string) ([]string, []string, []string, error) @@ -377,33 +377,39 @@ func FilterEventData(logs []types.Log, indexes []int, hexValues []string) []type return matches } -func (e executor) GetTokenIds(txIds []string) ([]string, error) { +func (e executor) GetTokenIds(txIds []string) ([]string, []string, error) { logs, err := e.GetEventData(txIds, "Transfer(address,address,uint256)") if err != nil { - return []string{}, libcommon.StringError(err) + return []string{}, []string{}, libcommon.StringError(err) } tokenIds := []string{} + addresses := []string{} for _, log := range logs { if len(log.Topics) != 4 { continue } + address := log.Address.String() + addresses = append(addresses, address) tokenId := new(big.Int).SetBytes(log.Topics[3].Bytes()) tokenIds = append(tokenIds, tokenId.String()) } - return tokenIds, nil + return tokenIds, addresses, nil } -func (e executor) GetTokenQuantities(txIds []string) ([]string, error) { +func (e executor) GetTokenQuantities(txIds []string) ([]string, []*big.Int, error) { logs, err := e.GetEventData(txIds, "Transfer(address,address,uint256)") if err != nil { - return []string{}, libcommon.StringError(err) + return []string{}, []*big.Int{}, libcommon.StringError(err) } - quantities := []string{} + quantities := []*big.Int{} + addresses := []string{} for _, log := range logs { + address := log.Address.String() + addresses = append(addresses, address) quantity := new(big.Int).SetBytes(log.Data) - quantities = append(quantities, quantity.String()) + quantities = append(quantities, quantity) } - return quantities, nil + return addresses, quantities, nil } func (e executor) ForwardNonFungibleTokens(txIds []string, recipient string) ([]string, []string, error) { diff --git a/pkg/service/transaction.go b/pkg/service/transaction.go index 12c3af08..5aebe1c6 100644 --- a/pkg/service/transaction.go +++ b/pkg/service/transaction.go @@ -64,27 +64,29 @@ func NewTransaction(repos repository.Repositories, redis database.RedisStore, un } type transactionProcessingData struct { - userId *string - user *model.User - deviceId *string - ip *string - platformId *string - executor *Executor - processingFeeAsset *model.Asset - transactionModel *model.Transaction - chain *Chain - executionRequest *model.ExecutionRequest - floatEstimate *model.Estimate[float64] - cardAuthorization *AuthorizedCharge - PaymentStatus checkout.PaymentStatus - PaymentId string - recipientWalletId *string - txIds []string - forwardTxIds []string - cumulativeValue *big.Int - trueGas uint64 - tokenIds string - tokenQuantities string + userId *string + user *model.User + deviceId *string + ip *string + platformId *string + executor *Executor + processingFeeAsset *model.Asset + transactionModel *model.Transaction + chain *Chain + executionRequest *model.ExecutionRequest + floatEstimate *model.Estimate[float64] + cardAuthorization *AuthorizedCharge + PaymentStatus checkout.PaymentStatus + PaymentId string + recipientWalletId *string + txIds []string + forwardTxIds []string + cumulativeValue *big.Int + trueGas uint64 + tokenIds string + tokenQuantities string + transferredTokens []string + transferredTokenQuantities []*big.Int } func (t transaction) Quote(ctx context.Context, d model.TransactionRequest, platformId string) (res model.Quote, err error) { @@ -98,7 +100,7 @@ func (t transaction) Quote(ctx context.Context, d model.TransactionRequest, plat return res, libcommon.StringError(err) } - allowed, err := t.isContractAllowed(ctx, platformId, chain.UUID, d) + allowed, highestType, err := t.isContractAllowed(ctx, platformId, chain.UUID, d) if err != nil { return res, libcommon.StringError(err) } @@ -119,6 +121,16 @@ func (t transaction) Quote(ctx context.Context, d model.TransactionRequest, plat res.Estimate = common.EstimateToPrecise(estimateUSD) executor.Close() + // Check if user has priviledge to execute this transaction and get the priviledge required + hasPriviledge, priviledgeLevel, err := UserHasPriviledge(ctx, estimateUSD.TotalUSD, highestType, d.UserAddress, t.repos) + if err != nil { + return res, libcommon.StringError(err) + } + res.PriviledgeLevel = priviledgeLevel + if !hasPriviledge { + return res, libcommon.StringError(errors.New("insufficient priviledge")) + } + // Sign entire payload bytes, err := json.Marshal(res) if err != nil { @@ -445,7 +457,7 @@ func (t transaction) postProcess(ctx context.Context, p transactionProcessingDat } // Get the Token IDs which were transferred - tokenIds, err := executor.GetTokenIds(p.txIds) + _ /*nftAddresses*/, tokenIds, err := executor.GetTokenIds(p.txIds) if err != nil { log.Err(err).Msg("Failed to get token ids") // TODO: Handle error instead of returning it @@ -464,14 +476,14 @@ func (t transaction) postProcess(ctx context.Context, p transactionProcessingDat } // Get the Token quantities which were transferred - tokenQuantities, err := executor.GetTokenQuantities(p.txIds) + p.transferredTokens, p.transferredTokenQuantities, err = executor.GetTokenQuantities(p.txIds) if err != nil { log.Err(err).Msg("Failed to get token quantities") // TODO: Handle error instead of returning it } - p.tokenQuantities = strings.Join(tokenQuantities, ",") + p.tokenQuantities = common.StringifyBigIntArray(p.transferredTokenQuantities) - if len(tokenQuantities) > 0 { + if len(p.transferredTokenQuantities) > 0 { forwardTxIds /*forwardTokenAddresses*/, _ /*tokenQuantities*/, _, err := executor.ForwardTokens(p.txIds, p.executionRequest.Quote.TransactionRequest.UserAddress) if err != nil { log.Err(err).Msg("Failed to forward tokens") @@ -496,6 +508,8 @@ func (t transaction) postProcess(ctx context.Context, p transactionProcessingDat // We can close the executor because we aren't using it after this executor.Close() + // Check true cost against quote + // Cache the gas associated with this transaction qc := NewQuoteCache(t.redis) err = qc.UpdateMaxCachedTrueGas(p.executionRequest.Quote.TransactionRequest, p.trueGas) @@ -863,11 +877,38 @@ func (t transaction) tenderTransaction(ctx context.Context, p transactionProcess cost := NewCost(t.redis, t.repos) trueWei := big.NewInt(0).Add(p.cumulativeValue, big.NewInt(int64(p.trueGas))) trueEth := common.WeiToEther(trueWei) - trueUSD, err := cost.LookupUSD(trueEth, p.chain.CoingeckoName, p.chain.CoincapName) + + // include true token cost in USD + tokenQuantities := []big.Int{} + for _, quantity := range p.transferredTokenQuantities { + tokenQuantities = append(tokenQuantities, *quantity) + } + trueEstimation := EstimationParams{ + ChainId: p.chain.ChainId, + CostETH: *p.cumulativeValue, + UseBuffer: false, + GasUsedWei: p.trueGas, + CostTokens: tokenQuantities, + TokenAddrs: p.transferredTokens, + } + presentValue, err := cost.EstimateTransaction(trueEstimation, *p.chain) if err != nil { return 0, libcommon.StringError(err) } - profit := p.floatEstimate.TotalUSD - trueUSD + profit := p.floatEstimate.TotalUSD - presentValue.TotalUSD + + // Check if priviledge matches the level assessed by the initial quote + priviledgeType := "NFT" + if len(tokenQuantities) > 0 { + priviledgeType = "TOKEN" + } + ok, level, err := UserHasPriviledge(ctx, presentValue.TotalUSD, priviledgeType, p.executionRequest.Quote.TransactionRequest.UserAddress, t.repos) + if err != nil { + return 0, libcommon.StringError(err) + } + if !ok || level > p.executionRequest.Quote.PriviledgeLevel { + MessageTeam("Transaction completed with insufficient priviledge: " + p.transactionModel.Id) + } // Create Receive Tx leg asset, err := t.repos.Asset.GetById(ctx, p.chain.GasTokenId) @@ -1037,17 +1078,21 @@ func (t *transaction) getStringInstrumentsAndUserId() { t.ids = GetStringIdsFromEnv() } -func (t transaction) isContractAllowed(ctx context.Context, platformId string, networkId string, request model.TransactionRequest) (isAllowed bool, err error) { +func (t transaction) isContractAllowed(ctx context.Context, platformId string, networkId string, request model.TransactionRequest) (isAllowed bool, highestType string, err error) { _, finish := Span(ctx, "service.transaction.isContractAllowed", SpanTag{"platformId": platformId}) defer finish() - + highestType = "NFT" for _, action := range request.Actions { cxAddr := action.CxAddr contract, err := t.repos.Contract.GetForValidation(ctx, cxAddr, networkId, platformId) if err != nil && err == serror.NOT_FOUND { - return false, libcommon.StringError(serror.CONTRACT_NOT_ALLOWED) + return false, highestType, libcommon.StringError(serror.CONTRACT_NOT_ALLOWED) } else if err != nil { - return false, libcommon.StringError(err) + return false, highestType, libcommon.StringError(err) + } + + if contract.Type == "TOKEN" { + highestType = "TOKEN" } if len(contract.Functions) == 0 { @@ -1061,5 +1106,5 @@ func (t transaction) isContractAllowed(ctx context.Context, platformId string, n } } - return true, nil + return true, highestType, nil } diff --git a/pkg/service/user_priviledge.go b/pkg/service/user_priviledge.go new file mode 100644 index 00000000..6ae0dbc9 --- /dev/null +++ b/pkg/service/user_priviledge.go @@ -0,0 +1,41 @@ +package service + +import ( + "context" + + "github.com/String-xyz/go-lib/v2/common" + "github.com/String-xyz/string-api/pkg/repository" +) + +func UserHasPriviledge(ctx context.Context, cost float64, assetType string, userAddress string, repos repository.Repositories) (bool, int, error) { + instrument, err := repos.Instrument.GetWalletByAddr(ctx, userAddress) + if err != nil { + return false, -1, common.StringError(err) + } + _ /*user*/, err = repos.User.GetById(ctx, instrument.UserId) + if err != nil { + return false, -1, common.StringError(err) + } + priviledge := 3 // TODO: Get this from user once it's in the table + + requirement := 0 + if assetType == "NFT" { + if cost < 1000.00 { + requirement = 1 + } else if cost < 5000.00 { + requirement = 2 + } else { + requirement = 3 + } + } else { + if cost < 5000.00 { + requirement = 2 + } else { + requirement = 3 + } + } + if priviledge >= requirement { + return true, requirement, nil + } + return false, requirement, nil +} From 7198e9ec0686e239f21ba860a4966ff939ed6bf2 Mon Sep 17 00:00:00 2001 From: Ocasta Date: Wed, 26 Jul 2023 16:39:27 -0500 Subject: [PATCH 04/20] create identity on user creation, and update on email verification --- pkg/service/user.go | 3 +++ pkg/service/verification.go | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/pkg/service/user.go b/pkg/service/user.go index cb04037a..de4d8107 100644 --- a/pkg/service/user.go +++ b/pkg/service/user.go @@ -124,6 +124,9 @@ func (u user) Create(ctx context.Context, request model.WalletSignaturePayloadSi return resp, libcommon.StringError(err) } + // Create a user identity for KYC + go u.repos.Identity.Create(ctx, model.Identity{UserId: user.Id}) + if device.Fingerprint != "" { // validate that device on user creation now := time.Now() diff --git a/pkg/service/verification.go b/pkg/service/verification.go index 23ed2a9e..816c7aa5 100644 --- a/pkg/service/verification.go +++ b/pkg/service/verification.go @@ -135,6 +135,9 @@ func (v verification) VerifyEmail(ctx context.Context, userId string, email stri // 5. Create user in Checkout go v.createCheckoutCustomer(ctx2, userId, platformId) + // 6. Update user identity + go v.updateIdentityEmail(ctx2, userId, now) + return nil } @@ -193,3 +196,26 @@ func (v verification) createCheckoutCustomer(ctx context.Context, userId string, return customerId } + +func (v verification) updateIdentityEmail(ctx context.Context, userId string, now time.Time) error { + identity, err := v.repos.Identity.GetByUserId(ctx, userId) + if err != nil { + if serror.Is(err, serror.NOT_FOUND) { + identity, err = v.repos.Identity.Create(ctx, model.Identity{UserId: userId}) + if err != nil { + log.Err(err).Msg("Failed to create identity") + return libcommon.StringError(err) + } + } else { + log.Err(err).Msg("Failed to get identity by user id") + return libcommon.StringError(err) + } + } + identity, err = v.repos.Identity.Update(ctx, identity.Id, model.IdentityUpdates{EmailVerified: &now}) + if err != nil { + log.Err(err).Msg("Failed to update identity") + return libcommon.StringError(err) + } + + return nil +} From 2507bdb325581565dda70faf33593861d2dd0ce5 Mon Sep 17 00:00:00 2001 From: Ocasta Date: Wed, 26 Jul 2023 18:20:36 -0500 Subject: [PATCH 05/20] add kyc service --- api/config.go | 3 ++ pkg/model/entity.go | 2 +- pkg/model/request.go | 2 +- pkg/service/base.go | 1 + pkg/service/kyc.go | 105 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 pkg/service/kyc.go diff --git a/api/config.go b/api/config.go index cd701b07..f81368a0 100644 --- a/api/config.go +++ b/api/config.go @@ -53,6 +53,8 @@ func NewServices(config APIConfig, repos repository.Repositories) service.Servic card := service.NewCard(repos) + kyc := service.NewKYC(repos) + return service.Services{ Auth: auth, Cost: cost, @@ -63,5 +65,6 @@ func NewServices(config APIConfig, repos repository.Repositories) service.Servic Verification: verification, Device: device, Card: card, + KYC: kyc, } } diff --git a/pkg/model/entity.go b/pkg/model/entity.go index c1a66ff1..3eecb944 100644 --- a/pkg/model/entity.go +++ b/pkg/model/entity.go @@ -26,7 +26,7 @@ type User struct { type Identity struct { Id string `json:"id,omitempty" db:"id"` - Level string `json:"level" db:"level"` + Level int `json:"level" db:"level"` AccountId string `json:"accountId" db:"account_id"` UserId string `json:"userId" db:"user_id"` CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` diff --git a/pkg/model/request.go b/pkg/model/request.go index 761dc1e0..e7877a6d 100644 --- a/pkg/model/request.go +++ b/pkg/model/request.go @@ -78,7 +78,7 @@ type UserUpdates struct { } type IdentityUpdates struct { - Level *string `json:"level" db:"level"` + Level *int `json:"level" db:"level"` AccountId *string `json:"accountId" db:"account_id"` UserId *string `json:"userId" db:"user_id"` EmailVerified *time.Time `json:"emailVerified,omitempty" db:"email_verified"` diff --git a/pkg/service/base.go b/pkg/service/base.go index b25d2bed..7550f301 100644 --- a/pkg/service/base.go +++ b/pkg/service/base.go @@ -15,4 +15,5 @@ type Services struct { Unit21 Unit21 Card Card Webhook Webhook + KYC KYC } diff --git a/pkg/service/kyc.go b/pkg/service/kyc.go new file mode 100644 index 00000000..5e444f8f --- /dev/null +++ b/pkg/service/kyc.go @@ -0,0 +1,105 @@ +package service + +import ( + "context" + + "github.com/String-xyz/string-api/pkg/model" + "github.com/String-xyz/string-api/pkg/repository" +) + +type KYC interface { + GetTransactionKYCLevel(assetType string, cost float64) int + GetUserKYCLevel(ctx context.Context, userId string) (level int, err error) + UpdateUserKYCLevel(ctx context.Context, userId string) (level int, err error) +} + +type kyc struct { + repos repository.Repositories +} + +func NewKYC(repos repository.Repositories) KYC { + return &kyc{repos} +} + +func (k kyc) UserMeetsKYCRequirements(ctx context.Context, userId string, assetType string, cost float64) (met bool, err error) { + transactionLevel := k.GetTransactionKYCLevel(assetType, cost) + + userLevel, err := k.GetUserKYCLevel(ctx, userId) + if err != nil { + return false, err + } + if userLevel >= transactionLevel { + return true, nil + } else { + return false, nil + } +} + +func (k kyc) GetTransactionKYCLevel(assetType string, cost float64) int { + if assetType == "NFT" { + if cost < 1000.00 { + return 1 + } else if cost < 5000.00 { + return 2 + } else { + return 3 + } + } else { + if cost < 5000.00 { + return 2 + } else { + return 3 + } + } +} + +func (k kyc) GetUserKYCLevel(ctx context.Context, userId string) (level int, err error) { + level, err = k.UpdateUserKYCLevel(ctx, userId) + if err != nil { + return level, err + } + + if err != nil { + return level, err + } + + return level, nil +} + +func (k kyc) UpdateUserKYCLevel(ctx context.Context, userId string) (level int, err error) { + identity, err := k.repos.Identity.GetByUserId(ctx, userId) + if err != nil { + return level, err + } + + if err != nil { + return level, err + } + + points := 0 + if identity.EmailVerified != nil { + points++ + } + if identity.PhoneVerified != nil { + points++ + } + if identity.DocumentVerified != nil { + points++ + } + if identity.SelfieVerified != nil { + points++ + } + + if points < 1 && level >= 1 { + identity.Level = 0 + k.repos.Identity.Update(ctx, userId, model.IdentityUpdates{Level: &identity.Level}) + } else if points >= 4 && level < 2 { + identity.Level = 2 + k.repos.Identity.Update(ctx, userId, model.IdentityUpdates{Level: &identity.Level}) + } else if points >= 1 && level < 1 && identity.EmailVerified != nil { + identity.Level = 1 + k.repos.Identity.Update(ctx, userId, model.IdentityUpdates{Level: &identity.Level}) + } + + return identity.Level, nil +} From 5825d48b1f5639744926c50bf5753b6463eed7ef Mon Sep 17 00:00:00 2001 From: akfoster Date: Wed, 26 Jul 2023 18:46:19 -0600 Subject: [PATCH 06/20] Update pkg/service/kyc.go Co-authored-by: saito-sv <7920256+saito-sv@users.noreply.github.com> --- pkg/service/kyc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/service/kyc.go b/pkg/service/kyc.go index 5e444f8f..682612d8 100644 --- a/pkg/service/kyc.go +++ b/pkg/service/kyc.go @@ -21,7 +21,7 @@ func NewKYC(repos repository.Repositories) KYC { return &kyc{repos} } -func (k kyc) UserMeetsKYCRequirements(ctx context.Context, userId string, assetType string, cost float64) (met bool, err error) { +func (k kyc) MeetsRequirements(ctx context.Context, userId string, assetType string, cost float64) (met bool, err error) { transactionLevel := k.GetTransactionKYCLevel(assetType, cost) userLevel, err := k.GetUserKYCLevel(ctx, userId) From 83ef00badaff92dd013ccc5bc4476d06c43e1d9a Mon Sep 17 00:00:00 2001 From: Ocasta Date: Wed, 26 Jul 2023 19:47:57 -0500 Subject: [PATCH 07/20] simplify KYC function names --- pkg/service/kyc.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pkg/service/kyc.go b/pkg/service/kyc.go index 682612d8..c11363a5 100644 --- a/pkg/service/kyc.go +++ b/pkg/service/kyc.go @@ -8,9 +8,10 @@ import ( ) type KYC interface { - GetTransactionKYCLevel(assetType string, cost float64) int - GetUserKYCLevel(ctx context.Context, userId string) (level int, err error) - UpdateUserKYCLevel(ctx context.Context, userId string) (level int, err error) + MeetsRequirements(ctx context.Context, userId string, assetType string, cost float64) (met bool, err error) + GetTransactionLevel(assetType string, cost float64) int + GetUserLevel(ctx context.Context, userId string) (level int, err error) + UpdateUserLevel(ctx context.Context, userId string) (level int, err error) } type kyc struct { @@ -22,9 +23,9 @@ func NewKYC(repos repository.Repositories) KYC { } func (k kyc) MeetsRequirements(ctx context.Context, userId string, assetType string, cost float64) (met bool, err error) { - transactionLevel := k.GetTransactionKYCLevel(assetType, cost) + transactionLevel := k.GetTransactionLevel(assetType, cost) - userLevel, err := k.GetUserKYCLevel(ctx, userId) + userLevel, err := k.GetUserLevel(ctx, userId) if err != nil { return false, err } @@ -35,7 +36,7 @@ func (k kyc) MeetsRequirements(ctx context.Context, userId string, assetType str } } -func (k kyc) GetTransactionKYCLevel(assetType string, cost float64) int { +func (k kyc) GetTransactionLevel(assetType string, cost float64) int { if assetType == "NFT" { if cost < 1000.00 { return 1 @@ -53,8 +54,8 @@ func (k kyc) GetTransactionKYCLevel(assetType string, cost float64) int { } } -func (k kyc) GetUserKYCLevel(ctx context.Context, userId string) (level int, err error) { - level, err = k.UpdateUserKYCLevel(ctx, userId) +func (k kyc) GetUserLevel(ctx context.Context, userId string) (level int, err error) { + level, err = k.UpdateUserLevel(ctx, userId) if err != nil { return level, err } @@ -66,7 +67,7 @@ func (k kyc) GetUserKYCLevel(ctx context.Context, userId string) (level int, err return level, nil } -func (k kyc) UpdateUserKYCLevel(ctx context.Context, userId string) (level int, err error) { +func (k kyc) UpdateUserLevel(ctx context.Context, userId string) (level int, err error) { identity, err := k.repos.Identity.GetByUserId(ctx, userId) if err != nil { return level, err From 27df6f7625099db6e849c653dbcdc3c0af3adce1 Mon Sep 17 00:00:00 2001 From: Ocasta Date: Thu, 27 Jul 2023 00:24:00 -0500 Subject: [PATCH 08/20] unit tests passing --- pkg/repository/identity.go | 4 +- pkg/service/kyc.go | 26 +++---- pkg/service/kyc_test.go | 138 +++++++++++++++++++++++++++++++++++++ 3 files changed, 151 insertions(+), 17 deletions(-) create mode 100644 pkg/service/kyc_test.go diff --git a/pkg/repository/identity.go b/pkg/repository/identity.go index 32d676d0..78ba76b7 100644 --- a/pkg/repository/identity.go +++ b/pkg/repository/identity.go @@ -67,7 +67,7 @@ func (i identity[T]) Update(ctx context.Context, id string, updates any) (identi } func (i identity[T]) GetByUserId(ctx context.Context, userId string) (identity model.Identity, err error) { - query := fmt.Sprintf("SELECT * FROM %s WHERE userId=$1", i.Table) + query := fmt.Sprintf("SELECT * FROM %s WHERE user_id=$1", i.Table) err = i.Store.QueryRowxContext(ctx, query, userId).StructScan(&identity) if err != nil { return identity, libcommon.StringError(err) @@ -76,7 +76,7 @@ func (i identity[T]) GetByUserId(ctx context.Context, userId string) (identity m } func (i identity[T]) GetByAccountId(ctx context.Context, accountId string) (identity model.Identity, err error) { - query := fmt.Sprintf("SELECT * FROM %s WHERE accountId=$1", i.Table) + query := fmt.Sprintf("SELECT * FROM %s WHERE account_id=$1", i.Table) err = i.Store.QueryRowxContext(ctx, query, accountId).StructScan(&identity) if err != nil { return identity, libcommon.StringError(err) diff --git a/pkg/service/kyc.go b/pkg/service/kyc.go index c11363a5..d767d830 100644 --- a/pkg/service/kyc.go +++ b/pkg/service/kyc.go @@ -60,10 +60,6 @@ func (k kyc) GetUserLevel(ctx context.Context, userId string) (level int, err er return level, err } - if err != nil { - return level, err - } - return level, nil } @@ -73,10 +69,6 @@ func (k kyc) UpdateUserLevel(ctx context.Context, userId string) (level int, err return level, err } - if err != nil { - return level, err - } - points := 0 if identity.EmailVerified != nil { points++ @@ -91,15 +83,19 @@ func (k kyc) UpdateUserLevel(ctx context.Context, userId string) (level int, err points++ } - if points < 1 && level >= 1 { + if points >= 4 { + if identity.Level != 2 { + identity.Level = 2 + k.repos.Identity.Update(ctx, userId, model.IdentityUpdates{Level: &identity.Level}) + } + } else if points >= 1 && identity.EmailVerified != nil { + if identity.Level != 1 { + identity.Level = 1 + k.repos.Identity.Update(ctx, userId, model.IdentityUpdates{Level: &identity.Level}) + } + } else if points <= 1 && identity.Level != 0 { identity.Level = 0 k.repos.Identity.Update(ctx, userId, model.IdentityUpdates{Level: &identity.Level}) - } else if points >= 4 && level < 2 { - identity.Level = 2 - k.repos.Identity.Update(ctx, userId, model.IdentityUpdates{Level: &identity.Level}) - } else if points >= 1 && level < 1 && identity.EmailVerified != nil { - identity.Level = 1 - k.repos.Identity.Update(ctx, userId, model.IdentityUpdates{Level: &identity.Level}) } return identity.Level, nil diff --git a/pkg/service/kyc_test.go b/pkg/service/kyc_test.go new file mode 100644 index 00000000..0c4f712c --- /dev/null +++ b/pkg/service/kyc_test.go @@ -0,0 +1,138 @@ +package service + +import ( + "context" + "database/sql" + "fmt" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + env "github.com/String-xyz/go-lib/v2/config" + "github.com/google/uuid" + "github.com/jmoiron/sqlx" + + "github.com/String-xyz/string-api/config" + "github.com/String-xyz/string-api/pkg/model" + "github.com/String-xyz/string-api/pkg/repository" + "github.com/stretchr/testify/assert" +) + +// define a test case struct +type kycCase struct { + CaseName string + User model.User + Identity model.Identity + AssetType string + Cost float64 + Met bool +} + +func getTestCases() (cases []kycCase) { + assetTypes := []string{"NFT", "TOKEN", "NFT_AND_TOKEN"} + assetCosts := []float64{0, 999.99, 1000.00, 4999.99, 5000.00, 100000.00} + kycLevels := []int{0, 1, 2, 3} + now := time.Now() + verifications := [][]*time.Time{ + {nil, nil, nil, nil}, + {&now, nil, nil, nil}, + {&now, &now, nil, nil}, + {&now, &now, &now, nil}, + {&now, &now, &now, &now}, + } + baseUser := model.User{ + Id: uuid.NewString(), + CreatedAt: now, + UpdatedAt: now, + Type: "User", + Status: "Onboarded", + Tags: nil, + FirstName: "Test", + MiddleName: "A", + LastName: "User", + Email: "FakeUser123@nomail.com", + } + + for _, assetType := range assetTypes { + for _, assetCost := range assetCosts { + for _, kycLevel := range kycLevels { + for i, verification := range verifications { + trueLevel := 0 + if i == 4 { + trueLevel = 2 + } else if i >= 1 { + trueLevel = 1 + } + met := (assetType == "NFT" && assetCost < 1000.00 && trueLevel >= 1) || (assetCost < 5000.00 && trueLevel >= 2) + testCase := kycCase{ + CaseName: fmt.Sprintf("AssetType: %s, AssetCost: %f, KYCLevel: %d, TrueLevel: %v", assetType, assetCost, kycLevel, trueLevel), + User: baseUser, + Identity: model.Identity{ + Id: uuid.NewString(), + Level: kycLevel, + AccountId: "", + UserId: "", + CreatedAt: now, + UpdatedAt: now, + DeletedAt: nil, + EmailVerified: verification[0], + PhoneVerified: verification[1], + SelfieVerified: verification[2], + DocumentVerified: verification[3], + }, + AssetType: assetType, + Cost: assetCost, + Met: met, + } + cases = append(cases, testCase) + } + } + } + } + + return cases +} + +func setup(t *testing.T) (kyc KYC, ctx context.Context, mock sqlmock.Sqlmock, db *sql.DB) { + env.LoadEnv(&config.Var, "../../.env") + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + sqlxDB := sqlx.NewDb(db, "sqlmock") + if err != nil { + t.Fatalf("error %s was not expected when opening stub db", err) + } + repos := repository.Repositories{ + Auth: repository.NewAuth(nil, sqlxDB), + Apikey: repository.NewApikey(sqlxDB), + User: repository.NewUser(sqlxDB), + Contact: repository.NewContact(sqlxDB), + Contract: repository.NewContract(sqlxDB), + Instrument: repository.NewInstrument(sqlxDB), + Device: repository.NewDevice(sqlxDB), + Asset: repository.NewAsset(sqlxDB), + Network: repository.NewNetwork(sqlxDB), + Transaction: repository.NewTransaction(sqlxDB), + TxLeg: repository.NewTxLeg(sqlxDB), + Location: repository.NewLocation(sqlxDB), + Platform: repository.NewPlatform(sqlxDB), + Identity: repository.NewIdentity(sqlxDB), + } + kyc = NewKYC(repos) + ctx = context.Background() + return kyc, ctx, mock, db +} + +func TestMeetsRequirements(t *testing.T) { + kyc, ctx, mock, db := setup(t) + defer db.Close() + testCases := getTestCases() + for _, tc := range testCases { + mockedIdentityRow := sqlmock.NewRows([]string{"id", "level", "account_id", "user_id", "email_verified", "phone_verified", "selfie_verified", "document_verified"}). + AddRow(tc.Identity.Id, tc.Identity.Level, tc.Identity.AccountId, tc.Identity.UserId, tc.Identity.EmailVerified, tc.Identity.PhoneVerified, tc.Identity.SelfieVerified, tc.Identity.DocumentVerified) + mock.ExpectQuery("SELECT * FROM identity WHERE user_id=$1").WithArgs(tc.User.Id).WillReturnRows(mockedIdentityRow) + + fmt.Printf("Running test for: %v\n", tc.CaseName) + met, err := kyc.MeetsRequirements(ctx, tc.User.Id, tc.AssetType, tc.Cost) + assert.NoError(t, err) + assert.Equal(t, tc.Met, met) + } +} From 22df8aa1135b914ffeb10f0541cf673f72f7c334 Mon Sep 17 00:00:00 2001 From: frostbournesb <85953565+frostbournesb@users.noreply.github.com> Date: Thu, 27 Jul 2023 16:26:57 -0400 Subject: [PATCH 09/20] add route --- api/handler/user.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/handler/user.go b/api/handler/user.go index d1aecb90..6b292920 100644 --- a/api/handler/user.go +++ b/api/handler/user.go @@ -390,6 +390,7 @@ func (u user) GetPersonaAccountId(c echo.Context) error { accountId, err := u.userService.GetPersonaAccountId(ctx, userId) if err != nil { + libcommon.LogStringError(c, err, "user: get persona account id") return httperror.Internal500(c) } return c.JSON(http.StatusOK, accountId) @@ -463,6 +464,7 @@ func (u user) RegisterRoutes(g *echo.Group, ms ...echo.MiddlewareFunc) { g.GET("/:id/status", u.Status, ms...) g.GET("/:id/verify-email", u.VerifyEmail, ms...) + g.GET("/persona-account-id", u.GetPersonaAccountId, ms...) g.PATCH("/:id", u.Update, ms...) } From 4fd4485b20e300961818eefae9874074b5d6c895 Mon Sep 17 00:00:00 2001 From: Ocasta Date: Thu, 27 Jul 2023 16:20:29 -0500 Subject: [PATCH 10/20] update annotations --- api/api.go | 4 ++++ api/handler/card.go | 2 +- api/handler/quotes.go | 2 +- api/handler/transact.go | 2 +- api/handler/user.go | 9 ++++----- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/api/api.go b/api/api.go index 8fa1cd6d..b64c4b9b 100644 --- a/api/api.go +++ b/api/api.go @@ -38,6 +38,10 @@ func heartbeat(c echo.Context) error { // @host string-api.xyz // @BasePath / + +// @SecurityDefinitions.api JWT +// @Scheme bearer +// @BearerFormat JWT func Start(config APIConfig) { e := echo.New() e.Validator = validator.New() diff --git a/api/handler/card.go b/api/handler/card.go index b3d29f46..58da89b4 100644 --- a/api/handler/card.go +++ b/api/handler/card.go @@ -30,7 +30,7 @@ func NewCard(route *echo.Echo, service service.Card) Card { // @Tags Cards // @Accept json // @Produce json -// @Security ApiKeyAuth +// @Security JWT // @Success 200 {object} []checkout.CardInstrument // @Failure 400 {object} error // @Failure 401 {object} error diff --git a/api/handler/quotes.go b/api/handler/quotes.go index 9e4d48d3..df2b4848 100644 --- a/api/handler/quotes.go +++ b/api/handler/quotes.go @@ -31,7 +31,7 @@ func NewQuote(route *echo.Echo, service service.Transaction) Quotes { // @Tags Transactions // @Accept json // @Produce json -// @Security ApiKeyAuth +// @Security JWT // @Param body body model.TransactionRequest true "Transaction Request" // @Success 200 {object} model.Quote // @Failure 400 {object} error diff --git a/api/handler/transact.go b/api/handler/transact.go index ee1364c2..d644277a 100644 --- a/api/handler/transact.go +++ b/api/handler/transact.go @@ -30,7 +30,7 @@ func NewTransaction(route *echo.Echo, service service.Transaction) Transaction { // @Tags Transactions // @Accept json // @Produce json -// @Security ApiKeyAuth +// @Security JWT // @Param saveCard query boolean false "do not save payment info" // @Param body body model.ExecutionRequest true "Execution Request" // @Success 200 {object} model.TransactionReceipt diff --git a/api/handler/user.go b/api/handler/user.go index 6b292920..25e95b17 100644 --- a/api/handler/user.go +++ b/api/handler/user.go @@ -114,7 +114,7 @@ func (u user) Create(c echo.Context) error { // @Tags Users // @Accept json // @Produce json -// @Security ApiKeyAuth +// @Security JWT // @Param id path string true "User ID" // @Success 200 {object} model.UserOnboardingStatus // @Failure 401 {object} error @@ -141,7 +141,7 @@ func (u user) Status(c echo.Context) error { // @Tags Users // @Accept json // @Produce json -// @Security ApiKeyAuth +// @Security JWT // @Param id path string true "User ID" // @Param body body model.UpdateUserName true "Update User Name" // @Success 200 {object} model.User @@ -187,7 +187,7 @@ func (u user) Update(c echo.Context) error { // @Tags Users // @Accept json // @Produce json -// @Security ApiKeyAuth +// @Security JWT // @Param id path string true "User ID" // @Param email query string true "Email to verify" // @Success 200 {object} ResultMessage @@ -373,8 +373,7 @@ func (u user) PreValidateEmail(c echo.Context) error { // @Tags Users // @Accept json // @Produce json -// @Security ApiKeyAuth -// @Param id path string true "User ID" +// @Security JWT // @Success 200 {object} string // @Failure 400 {object} error // @Failure 401 {object} error From 452a27e638102bade8bbea93d60fded647bfcdb8 Mon Sep 17 00:00:00 2001 From: Ocasta Date: Thu, 27 Jul 2023 17:10:39 -0500 Subject: [PATCH 11/20] ensure the identity actually get's built --- pkg/repository/identity.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/repository/identity.go b/pkg/repository/identity.go index 78ba76b7..113c3186 100644 --- a/pkg/repository/identity.go +++ b/pkg/repository/identity.go @@ -31,8 +31,8 @@ func NewIdentity(db database.Queryable) Identity { func (i identity[T]) Create(ctx context.Context, insert model.Identity) (identity model.Identity, err error) { query, args, err := i.Named(` - INSERT INTO identity (userId) - VALUES(:userId) RETURNING *`, insert) + INSERT INTO identity (user_id) + VALUES(:user_id) RETURNING *`, insert) if err != nil { return identity, libcommon.StringError(err) } From 7bd8b6b83ff3b5d70c1a0ea6c424a94aadad2467 Mon Sep 17 00:00:00 2001 From: Ocasta Date: Thu, 27 Jul 2023 17:34:26 -0500 Subject: [PATCH 12/20] handle noRows in sql selects --- pkg/repository/identity.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/repository/identity.go b/pkg/repository/identity.go index 113c3186..ae731670 100644 --- a/pkg/repository/identity.go +++ b/pkg/repository/identity.go @@ -2,6 +2,7 @@ package repository import ( "context" + "database/sql" "errors" "fmt" "strings" @@ -69,7 +70,7 @@ func (i identity[T]) Update(ctx context.Context, id string, updates any) (identi func (i identity[T]) GetByUserId(ctx context.Context, userId string) (identity model.Identity, err error) { query := fmt.Sprintf("SELECT * FROM %s WHERE user_id=$1", i.Table) err = i.Store.QueryRowxContext(ctx, query, userId).StructScan(&identity) - if err != nil { + if err != nil && err == sql.ErrNoRows { return identity, libcommon.StringError(err) } return identity, nil @@ -78,7 +79,7 @@ func (i identity[T]) GetByUserId(ctx context.Context, userId string) (identity m func (i identity[T]) GetByAccountId(ctx context.Context, accountId string) (identity model.Identity, err error) { query := fmt.Sprintf("SELECT * FROM %s WHERE account_id=$1", i.Table) err = i.Store.QueryRowxContext(ctx, query, accountId).StructScan(&identity) - if err != nil { + if err != nil && err == sql.ErrNoRows { return identity, libcommon.StringError(err) } return identity, nil From 679629996256912231ba241288b411c466f6edf2 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 27 Jul 2023 18:31:02 -0700 Subject: [PATCH 13/20] adjust logic to use KYC object --- pkg/model/transaction.go | 2 +- pkg/service/kyc.go | 10 ++++----- pkg/service/transaction.go | 32 +++++++++++++++++--------- pkg/service/user_priviledge.go | 41 ---------------------------------- 4 files changed, 27 insertions(+), 58 deletions(-) delete mode 100644 pkg/service/user_priviledge.go diff --git a/pkg/model/transaction.go b/pkg/model/transaction.go index 5de44f30..1da6b788 100644 --- a/pkg/model/transaction.go +++ b/pkg/model/transaction.go @@ -21,7 +21,7 @@ type Quote struct { TransactionRequest TransactionRequest `json:"request" validate:"required"` Estimate Estimate[string] `json:"estimate" validate:"required"` Signature string `json:"signature" validate:"required,base64"` - PriviledgeLevel int `json:"priviledgeLevel" validate:"required"` + Level int `json:"level" validate:"required"` } type ExecutionRequest struct { diff --git a/pkg/service/kyc.go b/pkg/service/kyc.go index d767d830..1d2b1711 100644 --- a/pkg/service/kyc.go +++ b/pkg/service/kyc.go @@ -8,7 +8,7 @@ import ( ) type KYC interface { - MeetsRequirements(ctx context.Context, userId string, assetType string, cost float64) (met bool, err error) + MeetsRequirements(ctx context.Context, userId string, assetType string, cost float64) (met bool, level int, err error) GetTransactionLevel(assetType string, cost float64) int GetUserLevel(ctx context.Context, userId string) (level int, err error) UpdateUserLevel(ctx context.Context, userId string) (level int, err error) @@ -22,17 +22,17 @@ func NewKYC(repos repository.Repositories) KYC { return &kyc{repos} } -func (k kyc) MeetsRequirements(ctx context.Context, userId string, assetType string, cost float64) (met bool, err error) { +func (k kyc) MeetsRequirements(ctx context.Context, userId string, assetType string, cost float64) (met bool, level int, err error) { transactionLevel := k.GetTransactionLevel(assetType, cost) userLevel, err := k.GetUserLevel(ctx, userId) if err != nil { - return false, err + return false, transactionLevel, err } if userLevel >= transactionLevel { - return true, nil + return true, transactionLevel, nil } else { - return false, nil + return false, transactionLevel, nil } } diff --git a/pkg/service/transaction.go b/pkg/service/transaction.go index 5aebe1c6..b209b2e9 100644 --- a/pkg/service/transaction.go +++ b/pkg/service/transaction.go @@ -121,14 +121,19 @@ func (t transaction) Quote(ctx context.Context, d model.TransactionRequest, plat res.Estimate = common.EstimateToPrecise(estimateUSD) executor.Close() - // Check if user has priviledge to execute this transaction and get the priviledge required - hasPriviledge, priviledgeLevel, err := UserHasPriviledge(ctx, estimateUSD.TotalUSD, highestType, d.UserAddress, t.repos) + userWallet, err := t.repos.Instrument.GetWalletByAddr(ctx, d.UserAddress) if err != nil { return res, libcommon.StringError(err) } - res.PriviledgeLevel = priviledgeLevel - if !hasPriviledge { - return res, libcommon.StringError(errors.New("insufficient priviledge")) + userId := userWallet.UserId + kyc := NewKYC(t.repos) + allowed, level, err := kyc.MeetsRequirements(ctx, userId, highestType, estimateUSD.TotalUSD) + if err != nil { + return res, libcommon.StringError(err) + } + res.Level = level + if !allowed { + return res, libcommon.StringError(errors.New("insufficient level")) } // Sign entire payload @@ -897,17 +902,22 @@ func (t transaction) tenderTransaction(ctx context.Context, p transactionProcess } profit := p.floatEstimate.TotalUSD - presentValue.TotalUSD - // Check if priviledge matches the level assessed by the initial quote - priviledgeType := "NFT" + assetType := "NFT" if len(tokenQuantities) > 0 { - priviledgeType = "TOKEN" + assetType = "TOKEN" + } + userWallet, err := t.repos.Instrument.GetWalletByAddr(ctx, p.executionRequest.Quote.TransactionRequest.UserAddress) + if err != nil { + return 0, libcommon.StringError(err) } - ok, level, err := UserHasPriviledge(ctx, presentValue.TotalUSD, priviledgeType, p.executionRequest.Quote.TransactionRequest.UserAddress, t.repos) + userId := userWallet.UserId + kyc := NewKYC(t.repos) + allowed, level, err := kyc.MeetsRequirements(ctx, userId, assetType, presentValue.TotalUSD) if err != nil { return 0, libcommon.StringError(err) } - if !ok || level > p.executionRequest.Quote.PriviledgeLevel { - MessageTeam("Transaction completed with insufficient priviledge: " + p.transactionModel.Id) + if !allowed || level > p.executionRequest.Quote.Level { + MessageTeam("Transaction completed with insufficient KYC: " + p.transactionModel.Id) } // Create Receive Tx leg diff --git a/pkg/service/user_priviledge.go b/pkg/service/user_priviledge.go deleted file mode 100644 index 6ae0dbc9..00000000 --- a/pkg/service/user_priviledge.go +++ /dev/null @@ -1,41 +0,0 @@ -package service - -import ( - "context" - - "github.com/String-xyz/go-lib/v2/common" - "github.com/String-xyz/string-api/pkg/repository" -) - -func UserHasPriviledge(ctx context.Context, cost float64, assetType string, userAddress string, repos repository.Repositories) (bool, int, error) { - instrument, err := repos.Instrument.GetWalletByAddr(ctx, userAddress) - if err != nil { - return false, -1, common.StringError(err) - } - _ /*user*/, err = repos.User.GetById(ctx, instrument.UserId) - if err != nil { - return false, -1, common.StringError(err) - } - priviledge := 3 // TODO: Get this from user once it's in the table - - requirement := 0 - if assetType == "NFT" { - if cost < 1000.00 { - requirement = 1 - } else if cost < 5000.00 { - requirement = 2 - } else { - requirement = 3 - } - } else { - if cost < 5000.00 { - requirement = 2 - } else { - requirement = 3 - } - } - if priviledge >= requirement { - return true, requirement, nil - } - return false, requirement, nil -} From cf8466e734c16d8ab110fcbbe19988cb2d79d903 Mon Sep 17 00:00:00 2001 From: Auroter <7332587+Auroter@users.noreply.github.com> Date: Thu, 27 Jul 2023 18:33:36 -0700 Subject: [PATCH 14/20] Update pkg/internal/common/util.go Co-authored-by: saito-sv <7920256+saito-sv@users.noreply.github.com> --- pkg/internal/common/util.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/internal/common/util.go b/pkg/internal/common/util.go index 2c955b0b..50cc9e63 100644 --- a/pkg/internal/common/util.go +++ b/pkg/internal/common/util.go @@ -85,7 +85,7 @@ func StringContainsAny(target string, substrs []string) bool { } func StringifyBigIntArray(arr []*big.Int) string { - strArr := []string{} + strArr := make([]string, len(arr)) for _, elem := range arr { strArr = append(strArr, elem.String()) } From 5c127cc6aec7d8234edee322cb99a6877b4cf4cf Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 31 Jul 2023 11:22:33 -0700 Subject: [PATCH 15/20] cast KYCLevel to int when comparing --- pkg/service/transaction.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/service/transaction.go b/pkg/service/transaction.go index b209b2e9..b68f96c8 100644 --- a/pkg/service/transaction.go +++ b/pkg/service/transaction.go @@ -131,7 +131,7 @@ func (t transaction) Quote(ctx context.Context, d model.TransactionRequest, plat if err != nil { return res, libcommon.StringError(err) } - res.Level = level + res.Level = int(level) if !allowed { return res, libcommon.StringError(errors.New("insufficient level")) } @@ -916,7 +916,7 @@ func (t transaction) tenderTransaction(ctx context.Context, p transactionProcess if err != nil { return 0, libcommon.StringError(err) } - if !allowed || level > p.executionRequest.Quote.Level { + if !allowed || int(level) > p.executionRequest.Quote.Level { MessageTeam("Transaction completed with insufficient KYC: " + p.transactionModel.Id) } From 65617abf406b51f027af6b2343824718c9778147 Mon Sep 17 00:00:00 2001 From: Ocasta Date: Mon, 31 Jul 2023 15:18:13 -0500 Subject: [PATCH 16/20] make webhook secret keys optional --- config/config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.go b/config/config.go index c0cd7079..5fbe95e4 100644 --- a/config/config.go +++ b/config/config.go @@ -8,6 +8,8 @@ type vars struct { SERVICE_NAME string `required:"false"` STRING_HOTWALLET_ADDRESS string `required:"false"` CARD_FAIL_PROBABILITY string `required:"false"` + CHECKOUT_WEBHOOK_SECRET_KEY string `required:"false"` + PERSONA_WEBHOOK_SECRET_KEY string `required:"false"` BASE_URL string `required:"true"` ENV string `required:"true"` PORT string `required:"true"` @@ -19,7 +21,6 @@ type vars struct { AWS_REGION string `required:"true"` AWS_KMS_KEY_ID string `required:"true"` CHECKOUT_PUBLIC_KEY string `required:"true"` - CHECKOUT_WEBHOOK_SECRET_KEY string `required:"true"` CHECKOUT_SECRET_KEY string `required:"true"` CHECKOUT_ENV string `required:"true"` EVM_PRIVATE_KEY string `required:"true"` @@ -51,7 +52,6 @@ type vars struct { RECEIPTS_EMAIL_ADDRESS string `required:"true"` SLACK_WEBHOOK_URL string `required:"true"` PERSONA_API_KEY string `required:"true"` - PERSONA_WEBHOOK_SECRET_KEY string `required:"true"` } var Var vars From 592043e9148db5d0e1b40442bf71c0e1acb57aba Mon Sep 17 00:00:00 2001 From: Ocasta Date: Mon, 31 Jul 2023 15:26:25 -0500 Subject: [PATCH 17/20] undo prior changes -- these are required --- config/config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.go b/config/config.go index 5fbe95e4..c0cd7079 100644 --- a/config/config.go +++ b/config/config.go @@ -8,8 +8,6 @@ type vars struct { SERVICE_NAME string `required:"false"` STRING_HOTWALLET_ADDRESS string `required:"false"` CARD_FAIL_PROBABILITY string `required:"false"` - CHECKOUT_WEBHOOK_SECRET_KEY string `required:"false"` - PERSONA_WEBHOOK_SECRET_KEY string `required:"false"` BASE_URL string `required:"true"` ENV string `required:"true"` PORT string `required:"true"` @@ -21,6 +19,7 @@ type vars struct { AWS_REGION string `required:"true"` AWS_KMS_KEY_ID string `required:"true"` CHECKOUT_PUBLIC_KEY string `required:"true"` + CHECKOUT_WEBHOOK_SECRET_KEY string `required:"true"` CHECKOUT_SECRET_KEY string `required:"true"` CHECKOUT_ENV string `required:"true"` EVM_PRIVATE_KEY string `required:"true"` @@ -52,6 +51,7 @@ type vars struct { RECEIPTS_EMAIL_ADDRESS string `required:"true"` SLACK_WEBHOOK_URL string `required:"true"` PERSONA_API_KEY string `required:"true"` + PERSONA_WEBHOOK_SECRET_KEY string `required:"true"` } var Var vars From 9430a557698c85f78a420401f9403364bb3126cb Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 31 Jul 2023 13:31:02 -0700 Subject: [PATCH 18/20] ensure unsigned quote payload is returned with 403 if KYC level is not sufficient --- api/handler/quotes.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/handler/quotes.go b/api/handler/quotes.go index df2b4848..4c5722e3 100644 --- a/api/handler/quotes.go +++ b/api/handler/quotes.go @@ -70,6 +70,11 @@ func (q quote) Quote(c echo.Context) error { } res, err := q.Service.Quote(ctx, body, platformId) + + if errors.Cause(err).Error() == "insufficient level" { // TODO: use a custom error + return c.JSON(http.StatusForbidden, res) + } + if err != nil { libcommon.LogStringError(c, err, "quote: quote") From c7277a3cc1e0369710a4558b8a983b16a5a0cefd Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 31 Jul 2023 13:41:10 -0700 Subject: [PATCH 19/20] stop dereferencing nil --- api/handler/quotes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/handler/quotes.go b/api/handler/quotes.go index 4c5722e3..8184daa1 100644 --- a/api/handler/quotes.go +++ b/api/handler/quotes.go @@ -71,7 +71,7 @@ func (q quote) Quote(c echo.Context) error { res, err := q.Service.Quote(ctx, body, platformId) - if errors.Cause(err).Error() == "insufficient level" { // TODO: use a custom error + if err != nil && errors.Cause(err).Error() == "insufficient level" { // TODO: use a custom error return c.JSON(http.StatusForbidden, res) } From c70cb3c81ec5b53f6eb2979ac94a46b0c4c2f8f1 Mon Sep 17 00:00:00 2001 From: Auroter <7332587+Auroter@users.noreply.github.com> Date: Mon, 31 Jul 2023 13:55:33 -0700 Subject: [PATCH 20/20] Update pkg/service/transaction.go Co-authored-by: akfoster --- pkg/service/transaction.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/service/transaction.go b/pkg/service/transaction.go index b68f96c8..82b332da 100644 --- a/pkg/service/transaction.go +++ b/pkg/service/transaction.go @@ -1101,7 +1101,7 @@ func (t transaction) isContractAllowed(ctx context.Context, platformId string, n return false, highestType, libcommon.StringError(err) } - if contract.Type == "TOKEN" { + if contract.Type == "TOKEN" || contract.Type == "NFT_AND_TOKEN" { highestType = "TOKEN" }