diff --git a/api/handler/quotes.go b/api/handler/quotes.go index df2b4848..8184daa1 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 err != nil && 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") diff --git a/pkg/internal/common/util.go b/pkg/internal/common/util.go index a5df5442..50cc9e63 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 := make([]string, len(arr)) + 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..1da6b788 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"` + Level int `json:"level" 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/kyc.go b/pkg/service/kyc.go index f8d9115f..6e953971 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 KYCLevel, err error) GetTransactionLevel(assetType string, cost float64) KYCLevel GetUserLevel(ctx context.Context, userId string) (level KYCLevel, err error) UpdateUserLevel(ctx context.Context, userId string) (level KYCLevel, err error) @@ -31,18 +31,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 KYCLevel, 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 - } else { - return false, nil + return true, transactionLevel, nil } +return false, transactionLevel, nil } func (k kyc) GetTransactionLevel(assetType string, cost float64) KYCLevel { diff --git a/pkg/service/transaction.go b/pkg/service/transaction.go index 12c3af08..82b332da 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,21 @@ func (t transaction) Quote(ctx context.Context, d model.TransactionRequest, plat res.Estimate = common.EstimateToPrecise(estimateUSD) executor.Close() + userWallet, err := t.repos.Instrument.GetWalletByAddr(ctx, d.UserAddress) + if err != nil { + return res, libcommon.StringError(err) + } + 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 = int(level) + if !allowed { + return res, libcommon.StringError(errors.New("insufficient level")) + } + // Sign entire payload bytes, err := json.Marshal(res) if err != nil { @@ -445,7 +462,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 +481,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 +513,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 +882,43 @@ 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 + + assetType := "NFT" + if len(tokenQuantities) > 0 { + assetType = "TOKEN" + } + userWallet, err := t.repos.Instrument.GetWalletByAddr(ctx, p.executionRequest.Quote.TransactionRequest.UserAddress) + if err != nil { + return 0, libcommon.StringError(err) + } + 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 !allowed || int(level) > p.executionRequest.Quote.Level { + MessageTeam("Transaction completed with insufficient KYC: " + p.transactionModel.Id) + } // Create Receive Tx leg asset, err := t.repos.Asset.GetById(ctx, p.chain.GasTokenId) @@ -1037,17 +1088,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" || contract.Type == "NFT_AND_TOKEN" { + highestType = "TOKEN" } if len(contract.Functions) == 0 { @@ -1061,5 +1116,5 @@ func (t transaction) isContractAllowed(ctx context.Context, platformId string, n } } - return true, nil + return true, highestType, nil }