diff --git a/api/handler/transact.go b/api/handler/transact.go index afcefcdf..7bd0db19 100644 --- a/api/handler/transact.go +++ b/api/handler/transact.go @@ -24,7 +24,7 @@ func NewTransaction(route *echo.Echo, service service.Transaction) Transaction { } func (t transaction) Transact(c echo.Context) error { - var body model.ExecutionRequest + var body model.PrecisionSafeExecutionRequest err := c.Bind(&body) if err != nil { LogStringError(c, err, "transact: execute bind") diff --git a/pkg/internal/common/precision.go b/pkg/internal/common/precision.go new file mode 100644 index 00000000..5f9ceb42 --- /dev/null +++ b/pkg/internal/common/precision.go @@ -0,0 +1,51 @@ +package common + +import ( + "strconv" + + "github.com/String-xyz/string-api/pkg/model" +) + +func QuoteToPrecise(imprecise model.Quote) model.PrecisionSafeQuote { + res := model.PrecisionSafeQuote{ + Timestamp: imprecise.Timestamp, + BaseUSD: strconv.FormatFloat(imprecise.BaseUSD, 'f', 2, 64), + GasUSD: strconv.FormatFloat(imprecise.GasUSD, 'f', 2, 64), + TokenUSD: strconv.FormatFloat(imprecise.TokenUSD, 'f', 2, 64), + ServiceUSD: strconv.FormatFloat(imprecise.ServiceUSD, 'f', 2, 64), + TotalUSD: strconv.FormatFloat(imprecise.TotalUSD, 'f', 2, 64), + } + return res +} + +func QuoteToImprecise(precise model.PrecisionSafeQuote) model.Quote { + res := model.Quote{ + Timestamp: precise.Timestamp, + } + res.BaseUSD, _ = strconv.ParseFloat(precise.BaseUSD, 64) + res.GasUSD, _ = strconv.ParseFloat(precise.GasUSD, 64) + res.TokenUSD, _ = strconv.ParseFloat(precise.TokenUSD, 64) + res.ServiceUSD, _ = strconv.ParseFloat(precise.ServiceUSD, 64) + res.TotalUSD, _ = strconv.ParseFloat(precise.TotalUSD, 64) + return res +} + +func ExecutionRequestToPrecise(imprecise model.ExecutionRequest) model.PrecisionSafeExecutionRequest { + res := model.PrecisionSafeExecutionRequest{ + TransactionRequest: imprecise.TransactionRequest, + PrecisionSafeQuote: QuoteToPrecise(imprecise.Quote), + Signature: imprecise.Signature, + CardToken: imprecise.CardToken, + } + return res +} + +func ExecutionRequestToImprecise(precise model.PrecisionSafeExecutionRequest) model.ExecutionRequest { + res := model.ExecutionRequest{ + TransactionRequest: precise.TransactionRequest, + Quote: QuoteToImprecise(precise.PrecisionSafeQuote), + Signature: precise.Signature, + CardToken: precise.CardToken, + } + return res +} diff --git a/pkg/model/transaction.go b/pkg/model/transaction.go index 1427ee41..793fe0ee 100644 --- a/pkg/model/transaction.go +++ b/pkg/model/transaction.go @@ -24,10 +24,26 @@ type ExecutionRequest struct { CardToken string `json:"cardToken"` } +type PrecisionSafeQuote struct { + Timestamp int64 `json:"timestamp"` + BaseUSD string `json:"baseUSD"` + GasUSD string `json:"gasUSD"` + TokenUSD string `json:"tokenUSD"` + ServiceUSD string `json:"serviceUSD"` + TotalUSD string `json:"totalUSD"` +} + +type PrecisionSafeExecutionRequest struct { + TransactionRequest + PrecisionSafeQuote + Signature string `json:"signature"` + CardToken string `json:"cardToken"` +} + // User will pass this in for a quote and receive Execution Parameters type TransactionRequest struct { UserAddress string `json:"userAddress"` // Used to keep track of user ie "0x44A4b9E2A69d86BA382a511f845CbF2E31286770" - ChainId int `json:"chainId"` // Chain ID to execute on e.g. 80000 + ChainId uint64 `json:"chainId"` // Chain ID to execute on e.g. 80000 CxAddr string `json:"contractAddress"` // Address of contract ie "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" CxFunc string `json:"contractFunction"` // Function declaration ie "mintTo(address) payable" CxReturn string `json:"contractReturn"` // Function return type ie "uint256" diff --git a/pkg/service/cost.go b/pkg/service/cost.go index 524c6104..630852f7 100644 --- a/pkg/service/cost.go +++ b/pkg/service/cost.go @@ -1,6 +1,7 @@ package service import ( + "math" "math/big" "os" "time" @@ -110,8 +111,18 @@ func (c cost) EstimateTransaction(p EstimationParams, chain Chain) (model.Quote, gasInUSD = 0.01 } + // Round up to nearest cent + transactionCost = centCeiling(transactionCost) + gasInUSD = centCeiling(gasInUSD) + tokenCost = centCeiling(tokenCost) + serviceFee = centCeiling(serviceFee) + + // sum total totalUSD := transactionCost + gasInUSD + tokenCost + serviceFee + // Round that up as well to account for any floating imprecision + totalUSD = centCeiling(totalUSD) + // Fill out CostEstimate and return return model.Quote{ Timestamp: timestamp, @@ -123,6 +134,10 @@ func (c cost) EstimateTransaction(p EstimationParams, chain Chain) (model.Quote, }, nil } +func centCeiling(value float64) float64 { + return math.Ceil(value*100) / 100 +} + func (c cost) getExternalAPICallInterval(rateLimitPerMinute float64, uniqueEntries uint32) int64 { return int64(float64(60*rateLimitPerMinute) / rateLimitPerMinute) } diff --git a/pkg/service/transaction.go b/pkg/service/transaction.go index 7c1cd03b..e98fa751 100644 --- a/pkg/service/transaction.go +++ b/pkg/service/transaction.go @@ -20,8 +20,8 @@ import ( ) type Transaction interface { - Quote(d model.TransactionRequest) (model.ExecutionRequest, error) - Execute(e model.ExecutionRequest, userId string, deviceId string, ip string) (model.TransactionReceipt, error) + Quote(d model.TransactionRequest) (model.PrecisionSafeExecutionRequest, error) + Execute(e model.PrecisionSafeExecutionRequest, userId string, deviceId string, ip string) (model.TransactionReceipt, error) } type TransactionRepos struct { @@ -55,27 +55,28 @@ func NewTransaction(repos repository.Repositories, redis store.RedisStore, unit2 } type transactionProcessingData struct { - userId *string - user *model.User - deviceId *string - ip *string - executor *Executor - processingFeeAsset *model.Asset - transactionModel *model.Transaction - chain *Chain - executionRequest *model.ExecutionRequest - cardAuthorization *AuthorizedCharge - cardCapture *payments.CapturesResponse - preBalance *float64 - recipientWalletId *string - txId *string - cumulativeValue *big.Int - trueGas *uint64 + userId *string + user *model.User + deviceId *string + ip *string + executor *Executor + processingFeeAsset *model.Asset + transactionModel *model.Transaction + chain *Chain + executionRequest *model.ExecutionRequest + precisionSafeExecutionRequest *model.PrecisionSafeExecutionRequest + cardAuthorization *AuthorizedCharge + cardCapture *payments.CapturesResponse + preBalance *float64 + recipientWalletId *string + txId *string + cumulativeValue *big.Int + trueGas *uint64 } -func (t transaction) Quote(d model.TransactionRequest) (model.ExecutionRequest, error) { +func (t transaction) Quote(d model.TransactionRequest) (model.PrecisionSafeExecutionRequest, error) { // TODO: use prefab service to parse d and fill out known params - res := model.ExecutionRequest{TransactionRequest: d} + res := model.PrecisionSafeExecutionRequest{TransactionRequest: d} // chain, err := model.ChainInfo(uint64(d.ChainId)) chain, err := ChainInfo(uint64(d.ChainId), t.repos.Network, t.repos.Asset) if err != nil { @@ -91,7 +92,7 @@ func (t transaction) Quote(d model.TransactionRequest) (model.ExecutionRequest, if err != nil { return res, common.StringError(err) } - res.Quote = estimateUSD + res.PrecisionSafeQuote = common.QuoteToPrecise(estimateUSD) executor.Close() // Sign entire payload @@ -108,9 +109,9 @@ func (t transaction) Quote(d model.TransactionRequest) (model.ExecutionRequest, return res, nil } -func (t transaction) Execute(e model.ExecutionRequest, userId string, deviceId string, ip string) (res model.TransactionReceipt, err error) { +func (t transaction) Execute(e model.PrecisionSafeExecutionRequest, userId string, deviceId string, ip string) (res model.TransactionReceipt, err error) { t.getStringInstrumentsAndUserId() - p := transactionProcessingData{executionRequest: &e, userId: &userId, deviceId: &deviceId, ip: &ip} + p := transactionProcessingData{precisionSafeExecutionRequest: &e, executionRequest: &model.ExecutionRequest{}, userId: &userId, deviceId: &deviceId, ip: &ip} // Pre-flight transaction setup p, err = t.transactionSetup(p) @@ -153,7 +154,7 @@ func (t transaction) transactionSetup(p transactionProcessingData) (transactionP p.user = &user // Pull chain info needed for execution from repository - chain, err := ChainInfo(uint64(p.executionRequest.ChainId), t.repos.Network, t.repos.Asset) + chain, err := ChainInfo(p.precisionSafeExecutionRequest.ChainId, t.repos.Network, t.repos.Asset) if err != nil { return p, common.StringError(err) } @@ -167,7 +168,7 @@ func (t transaction) transactionSetup(p transactionProcessingData) (transactionP p.transactionModel = &transactionModel updateDB := &model.TransactionUpdates{} - processingFeeAsset, err := t.populateInitialTxModelData(*p.executionRequest, updateDB) + processingFeeAsset, err := t.populateInitialTxModelData(*p.precisionSafeExecutionRequest, updateDB) p.processingFeeAsset = &processingFeeAsset if err != nil { return p, common.StringError(err) @@ -196,7 +197,7 @@ func (t transaction) transactionSetup(p transactionProcessingData) (transactionP func (t transaction) safetyCheck(p transactionProcessingData) (transactionProcessingData, error) { // Test the Tx and update model status - estimateUSD, estimateETH, err := t.testTransaction(*p.executor, p.executionRequest.TransactionRequest, *p.chain, false) + estimateUSD, estimateETH, err := t.testTransaction(*p.executor, p.precisionSafeExecutionRequest.TransactionRequest, *p.chain, false) if err != nil { return p, common.StringError(err) } @@ -206,7 +207,7 @@ func (t transaction) safetyCheck(p transactionProcessingData) (transactionProces } // Verify the Quote and update model status - _, err = verifyQuote(*p.executionRequest, estimateUSD) + _, err = verifyQuote(*p.precisionSafeExecutionRequest, estimateUSD) if err != nil { return p, common.StringError(err) } @@ -214,6 +215,7 @@ func (t transaction) safetyCheck(p transactionProcessingData) (transactionProces if err != nil { return p, common.StringError(err) } + *p.executionRequest = common.ExecutionRequestToImprecise(*p.precisionSafeExecutionRequest) // Get current balance of primary token preBalance, err := (*p.executor).GetBalance() @@ -442,7 +444,7 @@ func (t transaction) postProcess(p transactionProcessingData) { } } -func (t transaction) populateInitialTxModelData(e model.ExecutionRequest, m *model.TransactionUpdates) (model.Asset, error) { +func (t transaction) populateInitialTxModelData(e model.PrecisionSafeExecutionRequest, m *model.TransactionUpdates) (model.Asset, error) { txType := "fiat-to-crypto" m.Type = &txType // TODO populate transactionModel.Tags with key-val pairs for Unit21 @@ -510,7 +512,7 @@ func (t transaction) testTransaction(executor Executor, request model.Transactio return res, eth, nil } -func verifyQuote(e model.ExecutionRequest, newEstimate model.Quote) (bool, error) { +func verifyQuote(e model.PrecisionSafeExecutionRequest, newEstimate model.Quote) (bool, error) { // Null out values which have changed since payload was signed dataToValidate := e dataToValidate.Signature = "" @@ -529,7 +531,11 @@ func verifyQuote(e model.ExecutionRequest, newEstimate model.Quote) (bool, error if newEstimate.Timestamp-e.Timestamp > 20 { return false, common.StringError(errors.New("verifyQuote: quote expired")) } - if newEstimate.TotalUSD > e.TotalUSD { + quotedTotal, err := strconv.ParseFloat(e.TotalUSD, 64) + if err != nil { + return false, common.StringError(err) + } + if newEstimate.TotalUSD > quotedTotal { return false, common.StringError(errors.New("verifyQuote: price too volatile")) } return true, nil