Skip to content

task/sean/str-678 #240

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/handler/quotes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
9 changes: 9 additions & 0 deletions pkg/internal/common/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"math"
"math/big"
"strconv"
"strings"

Expand Down Expand Up @@ -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, ",")
}
1 change: 1 addition & 0 deletions pkg/model/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
26 changes: 16 additions & 10 deletions pkg/service/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down
11 changes: 5 additions & 6 deletions pkg/service/kyc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
121 changes: 88 additions & 33 deletions pkg/service/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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")
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -1061,5 +1116,5 @@ func (t transaction) isContractAllowed(ctx context.Context, platformId string, n
}
}

return true, nil
return true, highestType, nil
}