diff --git a/pkg/service/cost.go b/pkg/service/cost.go index 4dd16fbe..46ac70ad 100644 --- a/pkg/service/cost.go +++ b/pkg/service/cost.go @@ -107,19 +107,19 @@ func GetCoingeckoPlatformMapping() (map[uint64]string, map[string]uint64, error) return map[uint64]string{}, map[string]uint64{}, libcommon.StringError(err) } - id_to_platform := make(map[uint64]string) - platform_to_id := make(map[string]uint64) + idToPlatform := make(map[uint64]string) + platformToId := make(map[string]uint64) for _, p := range platforms { if p.ChainIdentifier != 0 { - id_to_platform[p.ChainIdentifier] = p.Id - platform_to_id[p.Id] = p.ChainIdentifier + idToPlatform[p.ChainIdentifier] = p.Id + platformToId[p.Id] = p.ChainIdentifier } } - return id_to_platform, platform_to_id, nil + return idToPlatform, platformToId, nil } func GetCoingeckoCoinMapping() (map[string]string, error) { - _, platform_to_id, err := GetCoingeckoPlatformMapping() + _, platformToId, err := GetCoingeckoPlatformMapping() if err != nil { return map[string]string{}, libcommon.StringError(err) } @@ -131,7 +131,7 @@ func GetCoingeckoCoinMapping() (map[string]string, error) { return map[string]string{}, libcommon.StringError(err) } - coin_key_to_id := make(map[string]string) + coinKeyToId := make(map[string]string) for _, coin := range coins { for key, val := range coin.Platforms { // There's some weird data floating around in here. Ignore it. @@ -139,14 +139,14 @@ func GetCoingeckoCoinMapping() (map[string]string, error) { continue } newKey := CoinKey{ - ChainId: platform_to_id[key], + ChainId: platformToId[key], Address: common.SanitizeChecksum(val), }.String() - coin_key_to_id[newKey] = coin.ID + coinKeyToId[newKey] = coin.ID } } - return coin_key_to_id, nil + return coinKeyToId, nil } // TODO: This logic is being reused, abstract it by templating and refactor diff --git a/pkg/service/cost_test.go b/pkg/service/cost_test.go index 143e26e8..9324b783 100644 --- a/pkg/service/cost_test.go +++ b/pkg/service/cost_test.go @@ -9,7 +9,7 @@ import ( func TestGetTokenPrices(t *testing.T) { keyMap, err := GetCoingeckoCoinMapping() assert.NoError(t, err) - name, ok := keyMap[CoinKey{ChainId: 43114, Address: "0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e"}] + name, ok := keyMap[CoinKey{ChainId: 43114, Address: "0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e"}.String()] assert.True(t, ok) assert.Equal(t, "usd-coin", name) } diff --git a/pkg/service/executor.go b/pkg/service/executor.go index 6dd62a74..10690cf5 100644 --- a/pkg/service/executor.go +++ b/pkg/service/executor.go @@ -158,17 +158,22 @@ func (e executor) TxWait(txIds []string) (uint64, error) { for _, txId := range txIds { txHash := ethcommon.HexToHash(txId) receipt := types.Receipt{} - for receipt.Status == 0 { + pending := true + for pending { pendingReceipt, err := e.geth.TransactionReceipt(context.Background(), txHash) // TransactionReceipt returns error "not found" while tx is pending if err != nil && err.Error() != "not found" { - return 0, libcommon.StringError(err) + return totalGasUsed, libcommon.StringError(err) } if pendingReceipt != nil { receipt = *pendingReceipt + pending = false } // TODO: Sleep for a few ms to keep the cpu cooler } + if receipt.Status == 0 { + return totalGasUsed, libcommon.StringError(errors.New("transaction failed")) + } totalGasUsed += receipt.GasUsed } return totalGasUsed, nil @@ -380,9 +385,6 @@ func (e executor) GetTokenIds(txIds []string) ([]string, error) { tokenId := new(big.Int).SetBytes(log.Topics[3].Bytes()) tokenIds = append(tokenIds, tokenId.String()) } - if len(tokenIds) == 0 { - return []string{}, libcommon.StringError(errors.New("no token ids found / no ERC721 transfer events found")) - } return tokenIds, nil } @@ -410,15 +412,13 @@ func (e executor) ForwardNonFungibleTokens(txIds []string, recipient string) ([] } // Filter events where recipient is our hot wallet toForward := FilterEventData(eventData, []int{2}, []string{hotWallet.String()}) - filteredTxIds := []string{} tokenIds := []string{} - gasUsed := big.NewInt(0) calls := []ContractCall{} for _, log := range toForward { tokenId := new(big.Int).SetBytes(log.Topics[3].Bytes()).String() call := ContractCall{ CxAddr: log.Address.String(), - CxFunc: "safeTransferFrom(address,address,uint256)", + CxFunc: "transferFrom(address,address,uint256)", CxParams: []string{ hotWallet.String(), recipient, @@ -430,16 +430,17 @@ func (e executor) ForwardNonFungibleTokens(txIds []string, recipient string) ([] } tokenIds = append(tokenIds, tokenId) calls = append(calls, call) + } + + forwardTxIds := []string{} + if len(calls) > 0 { + forwardTxIds, _, err = e.Initiate(calls) if err != nil { return txIds, tokenIds, libcommon.StringError(err) } } - forwardTxIds, gas, err := e.Initiate(calls) - filteredTxIds = append(filteredTxIds, forwardTxIds...) - gasUsed = gasUsed.Add(gasUsed, gas) - - return txIds, tokenIds, nil + return forwardTxIds, tokenIds, nil } func (e executor) ForwardTokens(txIds []string, recipient string) ([]string, []string, []string, error) { @@ -453,19 +454,16 @@ func (e executor) ForwardTokens(txIds []string, recipient string) ([]string, []s } // Filter events where recipient is our hot wallet toForward := FilterEventData(eventData, []int{2}, []string{hotWallet.String()}) - filteredTxIds := []string{} tokens := []string{} quantities := []string{} - gasUsed := big.NewInt(0) calls := []ContractCall{} for _, log := range toForward { quantity := new(big.Int).SetBytes(log.Data).String() token := log.Address.String() call := ContractCall{ CxAddr: token, - CxFunc: "safeTransferFrom(address,address,uint256)", + CxFunc: "transfer(address,uint256)", CxParams: []string{ - hotWallet.String(), recipient, quantity, }, @@ -476,20 +474,15 @@ func (e executor) ForwardTokens(txIds []string, recipient string) ([]string, []s tokens = append(tokens, token) quantities = append(quantities, quantity) calls = append(calls, call) - if err != nil { - return txIds, tokens, quantities, libcommon.StringError(err) - } } - // TODO: use this + forwardTxIds := []string{} if len(calls) > 0 { - forwardTxIds, gas, err := e.Initiate(calls) + forwardTxIds, _, err = e.Initiate(calls) if err != nil { return txIds, tokens, quantities, libcommon.StringError(err) } - filteredTxIds = append(filteredTxIds, forwardTxIds...) - gasUsed = gasUsed.Add(gasUsed, gas) } - return txIds, tokens, quantities, nil + return forwardTxIds, tokens, quantities, nil } diff --git a/pkg/service/quote_cache.go b/pkg/service/quote_cache.go index 48e0a44f..7931b660 100644 --- a/pkg/service/quote_cache.go +++ b/pkg/service/quote_cache.go @@ -16,7 +16,8 @@ import ( type QuoteCache interface { CheckUpdateCachedTransactionRequest(request model.TransactionRequest, desiredInterval int64) (recalculate bool, callEstimate CallEstimate, err error) - PutCachedTransactionRequest(request model.TransactionRequest, data CallEstimate) error + PutCachedTransactionRequest(request model.TransactionRequest, data CallEstimate) (CallEstimate, error) + UpdateMaxCachedTrueGas(request model.TransactionRequest, gas uint64) error } type quoteCache struct { @@ -50,18 +51,43 @@ func (q quoteCache) CheckUpdateCachedTransactionRequest(request model.Transactio } } -func (q quoteCache) PutCachedTransactionRequest(request model.TransactionRequest, data CallEstimate) error { - cacheObject := callEstimateCache{ +func (q quoteCache) UpdateMaxCachedTrueGas(request model.TransactionRequest, gas uint64) error { + key := tokenizeTransactionRequest(sanitizeTransactionRequest(request)) + cacheObject, err := store.GetObjectFromCache[callEstimateCache](q.redis, key) + if cacheObject.Timestamp == 0 || (err == nil && cacheObject.Gas < gas) { + cacheObject.Gas = gas + cacheObject.Timestamp = time.Now().Unix() + err = store.PutObjectInCache(q.redis, key, cacheObject) + } + if err != nil { + return libcommon.StringError(err) + } + return nil +} + +func (q quoteCache) PutCachedTransactionRequest(request model.TransactionRequest, data CallEstimate) (CallEstimate, error) { + key := tokenizeTransactionRequest(sanitizeTransactionRequest(request)) + cacheObject, err := store.GetObjectFromCache[callEstimateCache](q.redis, key) + if err != nil { + return CallEstimate{}, libcommon.StringError(err) + } + // Never lower the known gas value - this can come from estimation or a real transaction + if data.Gas < cacheObject.Gas { + data.Gas = cacheObject.Gas + } + cacheObject = callEstimateCache{ Timestamp: time.Now().Unix(), Value: data.Value.String(), Gas: data.Gas, Success: data.Success, } - err := store.PutObjectInCache(q.redis, tokenizeTransactionRequest(sanitizeTransactionRequest(request)), cacheObject) + err = store.PutObjectInCache(q.redis, tokenizeTransactionRequest(sanitizeTransactionRequest(request)), cacheObject) if err != nil { - return libcommon.StringError(err) + return CallEstimate{}, libcommon.StringError(err) } - return nil + value := big.NewInt(0) + value.SetString(cacheObject.Value, 10) + return CallEstimate{Value: *value, Gas: cacheObject.Gas, Success: cacheObject.Success}, nil } func sanitizeTransactionRequest(request model.TransactionRequest) model.TransactionRequest { diff --git a/pkg/service/transaction.go b/pkg/service/transaction.go index 830a4545..c81613e1 100644 --- a/pkg/service/transaction.go +++ b/pkg/service/transaction.go @@ -80,8 +80,9 @@ type transactionProcessingData struct { PaymentId string recipientWalletId *string txIds []string + forwardTxIds []string cumulativeValue *big.Int - trueGas *uint64 + trueGas uint64 tokenIds string tokenQuantities string } @@ -254,7 +255,7 @@ func (t transaction) safetyCheck(ctx context.Context, p transactionProcessingDat // Update cache if price is too volatile if errors.Cause(err).Error() == "verifyQuote: price too volatile" { quoteCache := NewQuoteCache(t.redis) - err = quoteCache.PutCachedTransactionRequest(p.executionRequest.Quote.TransactionRequest, estimateEVM) + _, err = quoteCache.PutCachedTransactionRequest(p.executionRequest.Quote.TransactionRequest, estimateEVM) if err != nil { return p, libcommon.StringError(err) } @@ -426,7 +427,7 @@ func (t transaction) postProcess(ctx context.Context, p transactionProcessingDat // confirm the Tx on the EVM trueGas, err := confirmTx(executor, p.txIds) - p.trueGas = &trueGas + p.trueGas = trueGas if err != nil { log.Err(err).Msg("Failed to confirm transaction") // TODO: Handle error instead of returning it @@ -443,7 +444,7 @@ func (t transaction) postProcess(ctx context.Context, p transactionProcessingDat // TODO: Handle error instead of returning it } - // Get the Token IDs which were transferred to the API + // Get the Token IDs which were transferred tokenIds, err := executor.GetTokenIds(p.txIds) if err != nil { log.Err(err).Msg("Failed to get token ids") @@ -454,21 +455,28 @@ func (t transaction) postProcess(ctx context.Context, p transactionProcessingDat // Forward any non fungible tokens received to the user // TODO: Use the TX ID/s from this in the receipt // TODO: Find a way to charge for the gas used in this transaction - if err == nil { // There will be an error if no ERC721 transfer events were detected - executor.ForwardNonFungibleTokens(p.txIds, p.executionRequest.Quote.TransactionRequest.UserAddress) + if len(tokenIds) > 0 { + forwardTxIds /*forwardTokenIds*/, _, err := executor.ForwardNonFungibleTokens(p.txIds, p.executionRequest.Quote.TransactionRequest.UserAddress) + if err != nil { + log.Err(err).Msg("Failed to forward non fungible tokens") + } + p.forwardTxIds = append(p.forwardTxIds, forwardTxIds...) } - // Get the Token quantities which were transferred to the API + // Get the Token quantities which were transferred tokenQuantities, 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, ",") - if err == nil { - executor.ForwardTokens(p.txIds, p.executionRequest.Quote.TransactionRequest.UserAddress) + if len(tokenQuantities) > 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") + } + p.forwardTxIds = append(p.forwardTxIds, forwardTxIds...) } // Cull any TXIDs from Approve(), the user and Unit21 and the receipt don't need them @@ -479,10 +487,22 @@ func (t transaction) postProcess(ctx context.Context, p transactionProcessingDat } // TODO: Get the final gas total here and cache it to the quote cache. And use it for subsequent quotes. + forwardGas, err := confirmTx(executor, p.forwardTxIds) + if err != nil { + log.Err(err).Msg("Failed to confirm forwarding transactions") + } + p.trueGas += forwardGas // We can close the executor because we aren't using it after this executor.Close() + // Cache the gas associated with this transaction + qc := NewQuoteCache(t.redis) + err = qc.UpdateMaxCachedTrueGas(p.executionRequest.Quote.TransactionRequest, p.trueGas) + if err != nil { + log.Err(err).Msg("Failed to update quote true gas cache") + } + // compute profit // TODO: factor request.processingFeeAsset in the event of crypto-to-usd profit, err := t.tenderTransaction(ctx, p) @@ -607,7 +627,7 @@ func (t transaction) testTransaction(executor Executor, request model.Transactio return res, 0, CallEstimate{}, libcommon.StringError(err) } if useCache { - err = quoteCache.PutCachedTransactionRequest(request, estimateEVM) + estimateEVM, err = quoteCache.PutCachedTransactionRequest(request, estimateEVM) if err != nil { return res, 0, CallEstimate{}, libcommon.StringError(err) } @@ -841,7 +861,7 @@ func (t transaction) tenderTransaction(ctx context.Context, p transactionProcess defer finish() cost := NewCost(t.redis) - trueWei := big.NewInt(0).Add(p.cumulativeValue, big.NewInt(int64(*p.trueGas))) + 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) if err != nil {