From 81fa3b5f0dd8fa8f896e7ec58dd1dbfcc04856a4 Mon Sep 17 00:00:00 2001 From: Sean Date: Sun, 2 Jul 2023 15:30:21 -0700 Subject: [PATCH 01/17] who did this --- pkg/model/transaction.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/model/transaction.go b/pkg/model/transaction.go index b99bdef6..2259133a 100644 --- a/pkg/model/transaction.go +++ b/pkg/model/transaction.go @@ -42,9 +42,9 @@ type TransactionRequest struct { ChainId uint64 `json:"chainId" validate:"required,number"` // Chain ID to execute on e.g. 80000. CxAddr string `json:"contractAddress" validate:"required,eth_addr"` // Address of contract ie "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" CxFunc string `json:"contractFunction" validate:"required"` // Function declaration ie "mintTo(address)" - CxReturn string `json:"contractReturn" validate:"required"` // Function return type ie "uint256" - CxParams []string `json:"contractParameters" validate:"required"` // Function parameters ie ["0x000000000000000000BEEF", "32"] - TxValue string `json:"txValue" validate:"required"` // Amount of native token to send ie "0.08 ether" + CxReturn string `json:"contractReturn"` // Function return type ie "uint256" + CxParams []string `json:"contractParameters"` // Function parameters ie ["0x000000000000000000BEEF", "32"] + TxValue string `json:"txValue"` // Amount of native token to send ie "0.08 ether" TxGasLimit string `json:"gasLimit" validate:"required,number"` // Gwei gas limit ie "210000 gwei" } From 4b3370d52206a023a049af01857145b8986938de Mon Sep 17 00:00:00 2001 From: Sean Date: Sun, 2 Jul 2023 17:22:31 -0700 Subject: [PATCH 02/17] adding erc20 forward logic --- pkg/service/executor.go | 61 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/pkg/service/executor.go b/pkg/service/executor.go index 7998f915..b039357c 100644 --- a/pkg/service/executor.go +++ b/pkg/service/executor.go @@ -44,8 +44,10 @@ type Executor interface { GetByChainId() (uint64, error) GetBalance() (float64, error) GetTokenIds(txId string) ([]string, error) + GetTokenQuantities(txId string) ([]string, error) GetEventData(txId string, eventSignature string) ([]types.Log, error) - ForwardTokens(txId string, recipient string) ([]string, []string, error) + ForwardNonFungibleTokens(txId string, recipient string) ([]string, []string, error) + ForwardTokens(txId string, recipient string) ([]string, []string, []string, error) } type executor struct { @@ -335,7 +337,20 @@ func (e executor) GetTokenIds(txId string) ([]string, error) { return tokenIds, nil } -func (e executor) ForwardTokens(txId string, recipient string) ([]string, []string, error) { +func (e executor) GetTokenQuantities(txId string) ([]string, error) { + logs, err := e.GetEventData(txId, "Transfer(address,address,uint256)") + if err != nil { + return []string{}, libcommon.StringError(err) + } + quantities := []string{} + for _, log := range logs { + quantity := new(big.Int).SetBytes(log.Data) + quantities = append(quantities, quantity.String()) + } + return quantities, nil +} + +func (e executor) ForwardNonFungibleTokens(txId string, recipient string) ([]string, []string, error) { eventData, err := e.GetEventData(txId, "Transfer(address,address,uint256)") if err != nil { return []string{}, []string{}, libcommon.StringError(err) @@ -373,3 +388,45 @@ func (e executor) ForwardTokens(txId string, recipient string) ([]string, []stri } return txIds, tokenIds, nil } + +func (e executor) ForwardTokens(txId string, recipient string) ([]string, []string, []string, error) { + eventData, err := e.GetEventData(txId, "Transfer(address,address,uint256)") + if err != nil { + return []string{}, []string{}, libcommon.StringError(err) + } + hotWallet, err := e.getAccount() + if err != nil { + return []string{}, []string{}, libcommon.StringError(err) + } + // Filter events where recipient is our hot wallet + toForward := FilterEventData(eventData, []int{2}, []string{hotWallet.String()}) + txIds := []string{} + tokens := []string{} + quantities := []string{} + gasUsed := big.NewInt(0) + 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)", + CxParams: []string{ + hotWallet.String(), + recipient, + quantity, + }, + CxReturn: "", + TxValue: "0", + TxGasLimit: "800000", + } + tokens = append(tokens, token) + quantities = append(quantities, quantity) + forwardTxId, gas, err := e.Initiate(call) + txIds = append(txIds, forwardTxId) + gasUsed = gasUsed.Add(gasUsed, gas) + if err != nil { + return txIds, tokens, quantities, libcommon.StringError(err) + } + } + return txIds, tokens, quantities, nil +} From f5afd8858776ef2b8fe833e42d50c3fa193a67ab Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 3 Jul 2023 13:11:27 -0700 Subject: [PATCH 03/17] Fixed post process aborting due to trying to get ERC721 data from ERC20 transfer events --- pkg/service/executor.go | 10 ++++++++-- pkg/service/transaction.go | 4 +++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pkg/service/executor.go b/pkg/service/executor.go index b039357c..6aae4bc2 100644 --- a/pkg/service/executor.go +++ b/pkg/service/executor.go @@ -331,9 +331,15 @@ func (e executor) GetTokenIds(txId string) ([]string, error) { } tokenIds := []string{} for _, log := range logs { + if len(log.Topics) != 4 { + continue + } 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 } @@ -392,11 +398,11 @@ func (e executor) ForwardNonFungibleTokens(txId string, recipient string) ([]str func (e executor) ForwardTokens(txId string, recipient string) ([]string, []string, []string, error) { eventData, err := e.GetEventData(txId, "Transfer(address,address,uint256)") if err != nil { - return []string{}, []string{}, libcommon.StringError(err) + return []string{}, []string{}, []string{}, libcommon.StringError(err) } hotWallet, err := e.getAccount() if err != nil { - return []string{}, []string{}, libcommon.StringError(err) + return []string{}, []string{}, []string{}, libcommon.StringError(err) } // Filter events where recipient is our hot wallet toForward := FilterEventData(eventData, []int{2}, []string{hotWallet.String()}) diff --git a/pkg/service/transaction.go b/pkg/service/transaction.go index 53aaff6b..0d1bbb4f 100644 --- a/pkg/service/transaction.go +++ b/pkg/service/transaction.go @@ -427,7 +427,9 @@ func (t transaction) postProcess(ctx context.Context, p transactionProcessingDat // Forward any 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 - executor.ForwardTokens(*p.txId, p.executionRequest.Quote.TransactionRequest.UserAddress) + if err == nil { // There will be an error if no ERC721 transfer events were detected + executor.ForwardTokens(*p.txId, p.executionRequest.Quote.TransactionRequest.UserAddress) + } // We can close the executor because we aren't using it after this executor.Close() From 65a050f76e6d46d5d51bc39bd615cb9e174c8399 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 5 Jul 2023 10:20:48 -0700 Subject: [PATCH 04/17] stashing --- api/handler/quotes.go | 11 +++-- pkg/model/transaction.go | 22 +++++---- pkg/service/executor.go | 98 ++++++++++++++++++++++++-------------- pkg/service/quote_cache.go | 32 +++++++------ pkg/service/transaction.go | 20 ++++---- 5 files changed, 111 insertions(+), 72 deletions(-) diff --git a/api/handler/quotes.go b/api/handler/quotes.go index 846731d5..9e4d48d3 100644 --- a/api/handler/quotes.go +++ b/api/handler/quotes.go @@ -55,10 +55,13 @@ func (q quote) Quote(c echo.Context) error { return httperror.InvalidPayload400(c, err) } - SanitizeChecksums(&body.CxAddr, &body.UserAddress) - // Sanitize Checksum for body.CxParams? It might look like this: - for i := range body.CxParams { - SanitizeChecksums(&body.CxParams[i]) + // TODO: See if there's a way to batch these into a single call of SanitizeChecksums + SanitizeChecksums(&body.UserAddress) + for i := range body.Actions { + SanitizeChecksums(&body.Actions[i].CxAddr, &body.UserAddress) + for j := range body.Actions[i].CxParams { + SanitizeChecksums(&body.Actions[i].CxParams[j]) + } } platformId, ok := c.Get("platformId").(string) diff --git a/pkg/model/transaction.go b/pkg/model/transaction.go index 2259133a..ef80aae5 100644 --- a/pkg/model/transaction.go +++ b/pkg/model/transaction.go @@ -37,15 +37,19 @@ type PaymentInfo struct { // User will pass this in for a quote and receive Execution Parameters type TransactionRequest struct { - UserAddress string `json:"userAddress" validate:"required,eth_addr"` // Used to keep track of user ie "0x44A4b9E2A69d86BA382a511f845CbF2E31286770" - AssetName string `json:"assetName" validate:"required,min=3,max=30"` // Used for receipt - ChainId uint64 `json:"chainId" validate:"required,number"` // Chain ID to execute on e.g. 80000. - CxAddr string `json:"contractAddress" validate:"required,eth_addr"` // Address of contract ie "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - CxFunc string `json:"contractFunction" validate:"required"` // Function declaration ie "mintTo(address)" - CxReturn string `json:"contractReturn"` // Function return type ie "uint256" - CxParams []string `json:"contractParameters"` // Function parameters ie ["0x000000000000000000BEEF", "32"] - TxValue string `json:"txValue"` // Amount of native token to send ie "0.08 ether" - TxGasLimit string `json:"gasLimit" validate:"required,number"` // Gwei gas limit ie "210000 gwei" + UserAddress string `json:"userAddress" validate:"required,eth_addr"` // Used to keep track of user ie "0x44A4b9E2A69d86BA382a511f845CbF2E31286770" + AssetName string `json:"assetName" validate:"required,min=3,max=30"` // Used for receipt + ChainId uint64 `json:"chainId" validate:"required,number"` // Chain ID to execute on e.g. 80000. + Actions []TransactionAction `json:"actions" validate:"required"` // Actions to execute +} + +type TransactionAction struct { + CxAddr string `json:"contractAddress" validate:"required,eth_addr"` // Address of contract ie "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + CxFunc string `json:"contractFunction" validate:"required"` // Function declaration ie "mintTo(address)" + CxReturn string `json:"contractReturn"` // Function return type ie "uint256" + CxParams []string `json:"contractParameters"` // Function parameters ie ["0x000000000000000000BEEF", "32"] + TxValue string `json:"txValue"` // Amount of native token to send ie "0.08 ether" + TxGasLimit string `json:"gasLimit" validate:"required,number"` // Gwei gas limit ie "210000 gwei" } type TransactionReceipt struct { diff --git a/pkg/service/executor.go b/pkg/service/executor.go index 6aae4bc2..9f85729d 100644 --- a/pkg/service/executor.go +++ b/pkg/service/executor.go @@ -37,9 +37,9 @@ type CallEstimate struct { type Executor interface { Initialize(network Chain) error - Initiate(call ContractCall) (string, *big.Int, error) - Estimate(call ContractCall) (CallEstimate, error) - TxWait(txId string) (uint64, error) + Initiate(calls []ContractCall) ([]string, *big.Int, error) + Estimate(calls []ContractCall) (CallEstimate, error) + TxWait(txIds []string) (uint64, error) Close() error GetByChainId() (uint64, error) GetBalance() (float64, error) @@ -83,54 +83,80 @@ func (e *executor) Close() error { return nil } -func (e executor) Estimate(call ContractCall) (CallEstimate, error) { - // Generate blockchain message - msg, err := e.generateTransactionMessage(call) - if err != nil { - return CallEstimate{}, libcommon.StringError(err) +func (e executor) Estimate(calls []ContractCall) (CallEstimate, error) { + // Generate blockchain messages + w3calls := []w3types.Caller{} + estimatedGasses := make([]uint64, len(calls)) + totalValue := big.NewInt(0) + for i, call := range calls { + // TODO: Further optimize this to take in the calls array + msg, err := e.generateTransactionMessage(call) + if err != nil { + return CallEstimate{}, libcommon.StringError(err) + } + totalValue = totalValue.Add(totalValue, msg.Value) + w3calls = append(w3calls, eth.EstimateGas(&msg, nil).Returns(&estimatedGasses[i])) } - - // Estimate gas of message - var estimatedGas uint64 - err = e.client.Call(eth.EstimateGas(&msg, nil).Returns(&estimatedGas)) + // Estimate gas of messages + err := e.client.Call(w3calls...) if err != nil { // Execution Will Revert! - return CallEstimate{Value: *msg.Value, Gas: estimatedGas, Success: false}, libcommon.StringError(err) + return CallEstimate{Value: *big.NewInt(0), Gas: 0, Success: false}, libcommon.StringError(err) + } + // Call(w3calls) should fill out estimatedGasses array + totalGas := uint64(0) + for _, gas := range estimatedGasses { + totalGas += gas } - return CallEstimate{Value: *msg.Value, Gas: estimatedGas, Success: true}, nil + return CallEstimate{Value: *totalValue, Gas: totalGas, Success: true}, nil } -func (e executor) Initiate(call ContractCall) (string, *big.Int, error) { - tx, err := e.generateTransactionRequest(call) - if err != nil { - return "", nil, libcommon.StringError(err) +func (e executor) Initiate(calls []ContractCall) ([]string, *big.Int, error) { + w3calls := []w3types.Caller{} + hashes := make([]ethcommon.Hash, len(calls)) + totalValue := big.NewInt(0) + for i, call := range calls { + // TODO: Further optimize this to take in the calls array + tx, err := e.generateTransactionRequest(call) + if err != nil { + return []string{}, nil, libcommon.StringError(err) + } + totalValue = totalValue.Add(totalValue, tx.Value()) + w3calls = append(w3calls, eth.SendTx(&tx).Returns(&hashes[i])) } - // Call tx and retrieve hash - var hash ethcommon.Hash - err = e.client.Call(eth.SendTx(&tx).Returns(&hash)) + // Call txs and retrieve hashes + err := e.client.Call(w3calls...) if err != nil { // Execution failed! - return "", nil, libcommon.StringError(err) + return []string{}, nil, libcommon.StringError(err) + } + hashStrings := make([]string, len(hashes)) + for i := range hashes { + hashStrings[i] = hashes[i].String() } - return hash.String(), tx.Value(), nil + return hashStrings, totalValue, nil } -func (e executor) TxWait(txId string) (uint64, error) { - txHash := ethcommon.HexToHash(txId) - receipt := types.Receipt{} - for receipt.Status == 0 { - 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) - } - if pendingReceipt != nil { - receipt = *pendingReceipt +func (e executor) TxWait(txIds []string) (uint64, error) { + totalGasUsed := uint64(0) + for _, txId := range txIds { + txHash := ethcommon.HexToHash(txId) + receipt := types.Receipt{} + for receipt.Status == 0 { + 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) + } + if pendingReceipt != nil { + receipt = *pendingReceipt + } + // TODO: Sleep for a few ms to keep the cpu cooler } - // TODO: Sleep for a few ms to keep the cpu cooler + totalGasUsed += receipt.GasUsed } - return receipt.GasUsed, nil + return totalGasUsed, nil } func (e executor) GetByChainId() (uint64, error) { diff --git a/pkg/service/quote_cache.go b/pkg/service/quote_cache.go index 8b397edc..48e0a44f 100644 --- a/pkg/service/quote_cache.go +++ b/pkg/service/quote_cache.go @@ -65,24 +65,28 @@ func (q quoteCache) PutCachedTransactionRequest(request model.TransactionRequest } func sanitizeTransactionRequest(request model.TransactionRequest) model.TransactionRequest { - // Structs are pointers + actions := []model.TransactionAction{} + for i := range request.Actions { + actions = append(actions, model.TransactionAction{ + CxAddr: request.Actions[i].CxAddr, + CxFunc: request.Actions[i].CxFunc, + CxReturn: request.Actions[i].CxReturn, + CxParams: append([]string{}, request.Actions[i].CxParams...), + TxValue: request.Actions[i].TxValue, + TxGasLimit: request.Actions[i].TxGasLimit, + }) + for j, param := range actions[i].CxParams { + if param == request.UserAddress { + actions[i].CxParams[j] = "*" + } + } + } sanitized := model.TransactionRequest{ UserAddress: request.UserAddress, + AssetName: request.AssetName, ChainId: request.ChainId, - CxAddr: request.CxAddr, - CxFunc: request.CxFunc, - CxReturn: request.CxReturn, - CxParams: append([]string{}, request.CxParams...), // So are arrays - TxValue: request.TxValue, // Get this from model.TransactionRequest because it requires no estimation - TxGasLimit: request.TxGasLimit, - } - // Treat the users address as a wildcard - for i, param := range sanitized.CxParams { - if param == sanitized.UserAddress { - sanitized.CxParams[i] = "*" - } + Actions: actions, } - sanitized.UserAddress = "*" return sanitized } diff --git a/pkg/service/transaction.go b/pkg/service/transaction.go index 0d1bbb4f..2943ba70 100644 --- a/pkg/service/transaction.go +++ b/pkg/service/transaction.go @@ -90,7 +90,6 @@ func (t transaction) Quote(ctx context.Context, d model.TransactionRequest, plat // TODO: use prefab service to parse d and fill out known params res.TransactionRequest = d - // chain, err := model.ChainInfo(uint64(d.ChainId)) chain, err := ChainInfo(ctx, uint64(d.ChainId), t.repos.Network, t.repos.Asset) if err != nil { return res, libcommon.StringError(err) @@ -531,16 +530,19 @@ func (t transaction) testTransaction(executor Executor, request model.Transactio } if recalculate { - call := ContractCall{ - CxAddr: request.CxAddr, - CxFunc: request.CxFunc, - CxReturn: request.CxReturn, - CxParams: request.CxParams, - TxValue: request.TxValue, - TxGasLimit: request.TxGasLimit, + calls := []ContractCall{} + for _, action := range request.Actions { + calls = append(calls, ContractCall{ + CxAddr: action.CxAddr, + CxFunc: action.CxFunc, + CxReturn: action.CxReturn, + CxParams: action.CxParams, + TxValue: action.TxValue, + TxGasLimit: action.TxGasLimit, + }) } // Estimate value and gas of Tx request - estimateEVM, err = executor.Estimate(call) + estimateEVM, err = executor.Estimate(calls) if err != nil { return res, 0, CallEstimate{}, libcommon.StringError(err) } From d0d802998b365a9915d359ec36b99d63288b27b1 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 6 Jul 2023 20:07:53 -0700 Subject: [PATCH 05/17] coingecko id lookup using address, enable multiple transactions --- api/handler/transact.go | 11 ++- pkg/internal/common/json.go | 14 +++ pkg/internal/emailer/emailer.go | 34 ++++---- pkg/model/transaction.go | 6 +- pkg/service/cost.go | 110 ++++++++++++++++++++---- pkg/service/cost_test.go | 15 ++++ pkg/service/executor.go | 72 +++++++++------- pkg/service/executor_test.go | 6 +- pkg/service/transaction.go | 145 +++++++++++++++++++++----------- 9 files changed, 287 insertions(+), 126 deletions(-) create mode 100644 pkg/service/cost_test.go diff --git a/api/handler/transact.go b/api/handler/transact.go index b70ce56e..ee1364c2 100644 --- a/api/handler/transact.go +++ b/api/handler/transact.go @@ -72,10 +72,13 @@ func (t transaction) Transact(c echo.Context) error { transactionRequest := body.Quote.TransactionRequest - SanitizeChecksums(&transactionRequest.CxAddr, &transactionRequest.UserAddress) - // Sanitize Checksum for body.CxParams? It might look like this: - for i := range transactionRequest.CxParams { - SanitizeChecksums(&transactionRequest.CxParams[i]) + // TODO: These should already be sanitized by the quote, double check when there's time + SanitizeChecksums(&transactionRequest.UserAddress) + for i := range transactionRequest.Actions { + SanitizeChecksums(&transactionRequest.Actions[i].CxAddr, &transactionRequest.UserAddress) + for j := range transactionRequest.Actions[i].CxParams { + SanitizeChecksums(&transactionRequest.Actions[i].CxParams[j]) + } } ip := c.RealIP() diff --git a/pkg/internal/common/json.go b/pkg/internal/common/json.go index fb3ba96c..813f87c5 100644 --- a/pkg/internal/common/json.go +++ b/pkg/internal/common/json.go @@ -53,6 +53,20 @@ func GetJsonGeneric(url string, target interface{}) error { return nil } +func GetJsonDebug(url string) (string, error) { + client := &http.Client{Timeout: 10 * time.Second} + response, err := client.Get(url) + if err != nil { + return "", libcommon.StringError(err) + } + defer response.Body.Close() + jsonData, err := io.ReadAll(response.Body) + if err != nil { + return "", libcommon.StringError(err) + } + return string(jsonData), nil +} + func parseJSON[T any](b []byte) (T, error) { var r T if err := json.Unmarshal(b, &r); err != nil { diff --git a/pkg/internal/emailer/emailer.go b/pkg/internal/emailer/emailer.go index 92bc9a75..418827bf 100644 --- a/pkg/internal/emailer/emailer.go +++ b/pkg/internal/emailer/emailer.go @@ -29,23 +29,23 @@ func New() Emailer { } type ReceiptGenerationParams struct { - ReceiptType string - CustomerName string - PaymentDescriptor string - TransactionDate string - StringPaymentId string - TransactionId string - TransactionExplorer string - DestinationAddress string - DestinationExplorer string - PaymentMethod string - Platform string - ItemOrdered string - TokenId string - Subtotal string - NetworkFee string - ProcessingFee string - Total string + ReceiptType string + CustomerName string + PaymentDescriptor string + TransactionDate string + StringPaymentId string + TransactionIds []string + TransactionExplorers []string + DestinationAddress string + DestinationExplorer string + PaymentMethod string + Platform string + ItemOrdered string + TokenIds string + Subtotal string + NetworkFee string + ProcessingFee string + Total string } func (e emailer) SendEmailVerification(ctx context.Context, email string, code string, platformName string) error { diff --git a/pkg/model/transaction.go b/pkg/model/transaction.go index ef80aae5..17d34554 100644 --- a/pkg/model/transaction.go +++ b/pkg/model/transaction.go @@ -53,7 +53,7 @@ type TransactionAction struct { } type TransactionReceipt struct { - TxId string `json:"txId"` - TxURL string `json:"txUrl"` - TxTimestamp string `json:"txTimestamp"` + TxIds []string `json:"txIds"` + TxURLs []string `json:"txUrls"` + TxTimestamp string `json:"txTimestamp"` } diff --git a/pkg/service/cost.go b/pkg/service/cost.go index 275fa19e..60f9c270 100644 --- a/pkg/service/cost.go +++ b/pkg/service/cost.go @@ -17,12 +17,12 @@ import ( ) type EstimationParams struct { - ChainId uint64 `json:"chainId"` - CostETH big.Int `json:"costETH"` - UseBuffer bool `json:"useBuffer"` - GasUsedWei uint64 `json:"gasUsedWei"` - CostToken big.Int `json:"costToken"` - TokenName string `json:"tokenName"` + ChainId uint64 `json:"chainId"` + CostETH big.Int `json:"costETH"` + UseBuffer bool `json:"useBuffer"` + GasUsedWei uint64 `json:"gasUsedWei"` + CostTokens []big.Int `json:"costToken"` + TokenAddrs []string `json:"tokenName"` } type OwlracleJSON struct { @@ -40,6 +40,25 @@ type OwlracleJSON struct { } `json:"speeds"` } +type CoingeckoPlatform struct { + Id string `json:"id"` + ChainIdentifier uint64 `json:"chain_identifier"` + Name string `json:"name"` + ShortName string `json:"shortname"` +} + +type CoingeckoCoin struct { + ID string `json:"id"` + Symbol string `json:"symbol"` + Name string `json:"name"` + Platforms map[string]string `json:"platforms"` +} + +type CoinKey struct { + ChainId uint64 + Address string +} + type CostCache struct { Timestamp int64 `json:"timestamp"` Value float64 `json:"value"` @@ -60,6 +79,53 @@ func NewCost(redis database.RedisStore) Cost { } } +// Get a huge list of data from coingecko to look up token names by address +func GetCoingeckoPlatformMapping() (map[uint64]string, map[string]uint64, error) { + // get list of platform names from coingecko to create mapping to chainid + var platforms []CoingeckoPlatform + err := common.GetJsonGeneric("https://api.coingecko.com/api/v3/asset_platforms", &platforms) + if err != nil { + return map[uint64]string{}, map[string]uint64{}, libcommon.StringError(err) + } + + id_to_platform := make(map[uint64]string) + platform_to_id := 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 + } + } + return id_to_platform, platform_to_id, nil +} + +func GetCoingeckoCoinMapping() (map[CoinKey]string, error) { + _, platform_to_id, err := GetCoingeckoPlatformMapping() + if err != nil { + return map[CoinKey]string{}, libcommon.StringError(err) + } + + // get list of platform names from coingecko to create mapping to chainid + var coins []CoingeckoCoin + err = common.GetJsonGeneric("https://api.coingecko.com/api/v3/coins/list?include_platform=true", &coins) + if err != nil { + return map[CoinKey]string{}, libcommon.StringError(err) + } + + coin_key_to_id := make(map[CoinKey]string) + for _, coin := range coins { + for key, val := range coin.Platforms { + newKey := CoinKey{ + ChainId: platform_to_id[key], + Address: val, + } + coin_key_to_id[newKey] = coin.ID + } + } + + return coin_key_to_id, nil +} + func (c cost) EstimateTransaction(p EstimationParams, chain Chain) (estimate model.Estimate[float64], err error) { // Get Unix Timestamp and chain info timestamp := time.Now().Unix() @@ -90,22 +156,32 @@ func (c cost) EstimateTransaction(p EstimationParams, chain Chain) (estimate mod gasInUSD *= 1.0 + common.GasBuffer(chain.ChainId) } - // Query cost of token in USD if used and apply buffer - costToken := common.WeiToEther(&p.CostToken) - // tokenCost in contract call ERC-20 token costs - // Also for buying tokens directly - tokenCost, err := c.LookupUSD(costToken, p.TokenName) + // // Query cost of token in USD if used and apply buffer + totalTokenCost := 0.0 + coinMapping, err := GetCoingeckoCoinMapping() if err != nil { return estimate, libcommon.StringError(err) } - if p.UseBuffer { - tokenCost *= 1.0 + common.TokenBuffer(p.TokenName) + for i, costToken := range p.CostTokens { + costTokenEth := common.WeiToEther(&costToken) + tokenName, ok := coinMapping[CoinKey{chain.ChainId, p.TokenAddrs[i]}] + if !ok { + return estimate, errors.New("token not found") + } + tokenCost, err := c.LookupUSD(costTokenEth, tokenName) + if err != nil { + return estimate, libcommon.StringError(err) + } + if p.UseBuffer { + tokenCost *= 1.0 + common.TokenBuffer(tokenName) + } + totalTokenCost += tokenCost } // Compute service fee upcharge := chain.StringFee baseCheckoutFee := 0.3 - serviceFee := (transactionCost+gasInUSD+tokenCost)*upcharge + baseCheckoutFee + serviceFee := (transactionCost+gasInUSD+totalTokenCost)*upcharge + baseCheckoutFee // floor if transactionCost < 0.01 { @@ -118,11 +194,11 @@ func (c cost) EstimateTransaction(p EstimationParams, chain Chain) (estimate mod // Round up to nearest cent transactionCost = centCeiling(transactionCost) gasInUSD = centCeiling(gasInUSD) - tokenCost = centCeiling(tokenCost) + totalTokenCost = centCeiling(totalTokenCost) serviceFee = centCeiling(serviceFee) // sum total - totalUSD := transactionCost + gasInUSD + tokenCost + serviceFee + totalUSD := transactionCost + gasInUSD + totalTokenCost + serviceFee // Round that up as well to account for any floating imprecision totalUSD = centCeiling(totalUSD) @@ -132,7 +208,7 @@ func (c cost) EstimateTransaction(p EstimationParams, chain Chain) (estimate mod Timestamp: timestamp, BaseUSD: transactionCost, GasUSD: gasInUSD, - TokenUSD: tokenCost, + TokenUSD: totalTokenCost, ServiceUSD: serviceFee, TotalUSD: totalUSD, }, nil diff --git a/pkg/service/cost_test.go b/pkg/service/cost_test.go new file mode 100644 index 00000000..143e26e8 --- /dev/null +++ b/pkg/service/cost_test.go @@ -0,0 +1,15 @@ +package service + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetTokenPrices(t *testing.T) { + keyMap, err := GetCoingeckoCoinMapping() + assert.NoError(t, err) + name, ok := keyMap[CoinKey{ChainId: 43114, Address: "0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e"}] + assert.True(t, ok) + assert.Equal(t, "usd-coin", name) +} diff --git a/pkg/service/executor.go b/pkg/service/executor.go index 9f85729d..3f54fc46 100644 --- a/pkg/service/executor.go +++ b/pkg/service/executor.go @@ -43,11 +43,11 @@ type Executor interface { Close() error GetByChainId() (uint64, error) GetBalance() (float64, error) - GetTokenIds(txId string) ([]string, error) - GetTokenQuantities(txId string) ([]string, error) - GetEventData(txId string, eventSignature string) ([]types.Log, error) - ForwardNonFungibleTokens(txId string, recipient string) ([]string, []string, error) - ForwardTokens(txId string, recipient string) ([]string, []string, []string, error) + GetTokenIds(txIds []string) ([]string, error) + GetTokenQuantities(txIds []string) ([]string, 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) } type executor struct { @@ -312,19 +312,21 @@ func (e executor) generateTransactionRequest(call ContractCall) (types.Transacti return tx, nil } -func (e executor) GetEventData(txId string, eventSignature string) ([]types.Log, error) { +func (e executor) GetEventData(txIds []string, eventSignature string) ([]types.Log, error) { events := []types.Log{} - receipt, err := e.geth.TransactionReceipt(context.Background(), ethcommon.HexToHash(txId)) - if err != nil { - return []types.Log{}, libcommon.StringError(err) - } + for _, txId := range txIds { + receipt, err := e.geth.TransactionReceipt(context.Background(), ethcommon.HexToHash(txId)) + if err != nil { + return []types.Log{}, libcommon.StringError(err) + } - event := crypto.Keccak256Hash([]byte(eventSignature)) + event := crypto.Keccak256Hash([]byte(eventSignature)) - // Iterate through the logs to find the transfer event and extract the token ID. - for _, log := range receipt.Logs { - if log.Topics[0].Hex() == event.Hex() { - events = append(events, *log) + // Iterate through the logs to find the transfer event and extract the token ID. + for _, log := range receipt.Logs { + if log.Topics[0].Hex() == event.Hex() { + events = append(events, *log) + } } } return events, nil @@ -350,8 +352,8 @@ func FilterEventData(logs []types.Log, indexes []int, hexValues []string) []type return matches } -func (e executor) GetTokenIds(txId string) ([]string, error) { - logs, err := e.GetEventData(txId, "Transfer(address,address,uint256)") +func (e executor) GetTokenIds(txIds []string) ([]string, error) { + logs, err := e.GetEventData(txIds, "Transfer(address,address,uint256)") if err != nil { return []string{}, libcommon.StringError(err) } @@ -369,8 +371,8 @@ func (e executor) GetTokenIds(txId string) ([]string, error) { return tokenIds, nil } -func (e executor) GetTokenQuantities(txId string) ([]string, error) { - logs, err := e.GetEventData(txId, "Transfer(address,address,uint256)") +func (e executor) GetTokenQuantities(txIds []string) ([]string, error) { + logs, err := e.GetEventData(txIds, "Transfer(address,address,uint256)") if err != nil { return []string{}, libcommon.StringError(err) } @@ -382,8 +384,8 @@ func (e executor) GetTokenQuantities(txId string) ([]string, error) { return quantities, nil } -func (e executor) ForwardNonFungibleTokens(txId string, recipient string) ([]string, []string, error) { - eventData, err := e.GetEventData(txId, "Transfer(address,address,uint256)") +func (e executor) ForwardNonFungibleTokens(txIds []string, recipient string) ([]string, []string, error) { + eventData, err := e.GetEventData(txIds, "Transfer(address,address,uint256)") if err != nil { return []string{}, []string{}, libcommon.StringError(err) } @@ -393,9 +395,10 @@ func (e executor) ForwardNonFungibleTokens(txId string, recipient string) ([]str } // Filter events where recipient is our hot wallet toForward := FilterEventData(eventData, []int{2}, []string{hotWallet.String()}) - txIds := []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{ @@ -411,18 +414,21 @@ func (e executor) ForwardNonFungibleTokens(txId string, recipient string) ([]str TxGasLimit: "800000", } tokenIds = append(tokenIds, tokenId) - forwardTxId, gas, err := e.Initiate(call) - txIds = append(txIds, forwardTxId) - gasUsed = gasUsed.Add(gasUsed, gas) + calls = append(calls, call) 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 } -func (e executor) ForwardTokens(txId string, recipient string) ([]string, []string, []string, error) { - eventData, err := e.GetEventData(txId, "Transfer(address,address,uint256)") +func (e executor) ForwardTokens(txIds []string, recipient string) ([]string, []string, []string, error) { + eventData, err := e.GetEventData(txIds, "Transfer(address,address,uint256)") if err != nil { return []string{}, []string{}, []string{}, libcommon.StringError(err) } @@ -432,10 +438,11 @@ func (e executor) ForwardTokens(txId string, recipient string) ([]string, []stri } // Filter events where recipient is our hot wallet toForward := FilterEventData(eventData, []int{2}, []string{hotWallet.String()}) - txIds := []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() @@ -453,12 +460,15 @@ func (e executor) ForwardTokens(txId string, recipient string) ([]string, []stri } tokens = append(tokens, token) quantities = append(quantities, quantity) - forwardTxId, gas, err := e.Initiate(call) - txIds = append(txIds, forwardTxId) - gasUsed = gasUsed.Add(gasUsed, gas) + calls = append(calls, call) if err != nil { return txIds, tokens, quantities, libcommon.StringError(err) } } + + forwardTxIds, gas, err := e.Initiate(calls) + filteredTxIds = append(filteredTxIds, forwardTxIds...) + gasUsed = gasUsed.Add(gasUsed, gas) + return txIds, tokens, quantities, nil } diff --git a/pkg/service/executor_test.go b/pkg/service/executor_test.go index da5b2be6..610baac9 100644 --- a/pkg/service/executor_test.go +++ b/pkg/service/executor_test.go @@ -29,7 +29,7 @@ func TestGetEventData(t *testing.T) { e, err := setupTest() assert.NoError(t, err) - eventData, err := e.GetEventData("0xe27a6a4b4ee6cbd51242faf21044941de70f5ba65ea86673d7abde75eb6c2f56", + eventData, err := e.GetEventData([]string{"0xe27a6a4b4ee6cbd51242faf21044941de70f5ba65ea86673d7abde75eb6c2f56"}, "Transfer(address,address,uint256)") assert.NoError(t, err) assert.Equal(t, "0x00000000000000000000000044a4b9e2a69d86ba382a511f845cbf2e31286770", eventData[0].Topics[2].Hex()) @@ -39,7 +39,7 @@ func TestGetTokensTransferred(t *testing.T) { e, err := setupTest() assert.NoError(t, err) - tokens, err := e.GetTokenIds("0xe27a6a4b4ee6cbd51242faf21044941de70f5ba65ea86673d7abde75eb6c2f56") + tokens, err := e.GetTokenIds([]string{"0xe27a6a4b4ee6cbd51242faf21044941de70f5ba65ea86673d7abde75eb6c2f56"}) assert.NoError(t, err) assert.Equal(t, []string{"167"}, tokens) @@ -49,7 +49,7 @@ func TestFilterEventData(t *testing.T) { e, err := setupTest() assert.NoError(t, err) - eventData, err := e.GetEventData("0xe27a6a4b4ee6cbd51242faf21044941de70f5ba65ea86673d7abde75eb6c2f56", + eventData, err := e.GetEventData([]string{"0xe27a6a4b4ee6cbd51242faf21044941de70f5ba65ea86673d7abde75eb6c2f56"}, "Transfer(address,address,uint256)") assert.NoError(t, err) diff --git a/pkg/service/transaction.go b/pkg/service/transaction.go index 2943ba70..a9db7e86 100644 --- a/pkg/service/transaction.go +++ b/pkg/service/transaction.go @@ -13,6 +13,7 @@ import ( libcommon "github.com/String-xyz/go-lib/v2/common" "github.com/String-xyz/go-lib/v2/database" serror "github.com/String-xyz/go-lib/v2/stringerror" + "github.com/lmittmann/w3" "github.com/String-xyz/string-api/pkg/internal/checkout" "github.com/String-xyz/string-api/pkg/internal/common" @@ -78,7 +79,7 @@ type transactionProcessingData struct { PaymentStatus checkout.PaymentStatus PaymentId string recipientWalletId *string - txId *string + txIds []string cumulativeValue *big.Int trueGas *uint64 tokenIds string @@ -162,7 +163,13 @@ func (t transaction) Execute(ctx context.Context, e model.ExecutionRequest, user ctx2 := context.Background() go t.postProcess(ctx2, p) - return model.TransactionReceipt{TxId: *p.txId, TxURL: p.chain.Explorer + "/tx/" + *p.txId, TxTimestamp: time.Now().Format(time.RFC1123)}, nil + ids := []string{} + urls := []string{} + for _, id := range p.txIds { + ids = append(ids, id) + urls = append(urls, p.chain.Explorer+"/tx/"+id) + } + return model.TransactionReceipt{TxIds: ids, TxURLs: urls, TxTimestamp: time.Now().Format(time.RFC1123)}, nil } func (t transaction) transactionSetup(ctx context.Context, p transactionProcessingData) (transactionProcessingData, error) { @@ -324,21 +331,26 @@ func (t transaction) initiateTransaction(ctx context.Context, p transactionProce defer finish() request := p.executionRequest.Quote.TransactionRequest - call := ContractCall{ - CxAddr: request.CxAddr, - CxFunc: request.CxFunc, - CxReturn: request.CxReturn, - CxParams: request.CxParams, - TxValue: request.TxValue, - TxGasLimit: request.TxGasLimit, + calls := []ContractCall{} + for _, action := range request.Actions { + + call := ContractCall{ + CxAddr: action.CxAddr, + CxFunc: action.CxFunc, + CxReturn: action.CxReturn, + CxParams: action.CxParams, + TxValue: action.TxValue, + TxGasLimit: action.TxGasLimit, + } + calls = append(calls, call) } - txId, value, err := (*p.executor).Initiate(call) + txIds, value, err := (*p.executor).Initiate(calls) p.cumulativeValue = value if err != nil { return p, libcommon.StringError(err) } - p.txId = &txId + p.txIds = append(p.txIds, txIds...) // Create Response Tx leg eth := common.WeiToEther(value) @@ -364,7 +376,8 @@ func (t transaction) initiateTransaction(ctx context.Context, p transactionProce status := "Transaction Initiated" txAmount := p.cumulativeValue.String() - updateDB := &model.TransactionUpdates{Status: &status, TransactionHash: p.txId, TransactionAmount: &txAmount} + hashes := strings.Join(p.txIds, ", ") + updateDB := &model.TransactionUpdates{Status: &status, TransactionHash: &hashes, TransactionAmount: &txAmount} err = t.repos.Transaction.Update(ctx, p.transactionModel.Id, updateDB) if err != nil { return p, libcommon.StringError(err) @@ -397,7 +410,7 @@ func (t transaction) postProcess(ctx context.Context, p transactionProcessingDat } // confirm the Tx on the EVM - trueGas, err := confirmTx(executor, *p.txId) + trueGas, err := confirmTx(executor, p.txIds) p.trueGas = &trueGas if err != nil { log.Err(err).Msg("Failed to confirm transaction") @@ -416,7 +429,7 @@ func (t transaction) postProcess(ctx context.Context, p transactionProcessingDat } // Get the Token IDs which were transferred - tokenIds, err := executor.GetTokenIds(*p.txId) + 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 @@ -427,7 +440,7 @@ func (t transaction) postProcess(ctx context.Context, p transactionProcessingDat // 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.ForwardTokens(*p.txId, p.executionRequest.Quote.TransactionRequest.UserAddress) + executor.ForwardTokens(p.txIds, p.executionRequest.Quote.TransactionRequest.UserAddress) } // We can close the executor because we aren't using it after this @@ -502,9 +515,19 @@ func (t transaction) populateInitialTxModelData(ctx context.Context, e model.Exe // TODO populate transactionModel.PlatformId with UUID of customer // bytes, err := json.Marshal() - contractParams := pq.StringArray(e.Quote.TransactionRequest.CxParams) + // For now just concat everything + concatParams := []string{} + concatFuncs := []string{} + for _, action := range e.Quote.TransactionRequest.Actions { + concatParams = append(concatParams, "[") + concatParams = append(concatParams, action.CxParams...) + concatParams = append(concatParams, "]") + concatFuncs = append(concatFuncs, action.CxFunc+action.CxReturn) + } + + contractParams := pq.StringArray(concatParams) m.ContractParams = &contractParams - contractFunc := e.Quote.TransactionRequest.CxFunc + e.Quote.TransactionRequest.CxReturn + contractFunc := strings.Join(concatFuncs, ",") m.ContractFunc = &contractFunc asset, err := t.repos.Asset.GetByName(ctx, "USD") @@ -554,6 +577,17 @@ func (t transaction) testTransaction(executor Executor, request model.Transactio } } + // Factor in approvals to Token Cost + tokenAddresses := []string{} + tokenAmounts := []big.Int{} + for _, action := range request.Actions { + if strings.ToLower(strings.ReplaceAll(action.CxFunc, " ", "")) == "approve(address,uint256)" { + tokenAddresses = append(tokenAddresses, action.CxAddr) + // It should be safe at this point to w3.I without panic + tokenAmounts = append(tokenAmounts, *w3.I(action.CxParams[1])) + } + } + // Calculate total eth estimate as float64 gas := new(big.Int) gas.SetUint64(estimateEVM.Gas) @@ -570,8 +604,8 @@ func (t transaction) testTransaction(executor Executor, request model.Transactio CostETH: estimateEVM.Value, UseBuffer: useBuffer, GasUsedWei: estimateEVM.Gas, - CostToken: *big.NewInt(0), - TokenName: "", + CostTokens: tokenAmounts, + TokenAddrs: tokenAddresses, } // Estimate Cost in USD to execute Tx request @@ -756,8 +790,8 @@ func (t transaction) authCard(ctx context.Context, p transactionProcessingData) return p, nil } -func confirmTx(executor Executor, txId string) (uint64, error) { - trueGas, err := executor.TxWait(txId) +func confirmTx(executor Executor, txIds []string) (uint64, error) { + trueGas, err := executor.TxWait(txIds) if err != nil { return 0, libcommon.StringError(err) } @@ -871,24 +905,29 @@ func (t transaction) sendEmailReceipt(ctx context.Context, p transactionProcessi transactionRequest := p.executionRequest.Quote.TransactionRequest estimate := p.floatEstimate + explorers := []string{} + for _, id := range p.txIds { + explorers = append(explorers, p.chain.Explorer+"/tx"+id) + } + receiptParams := emailer.ReceiptGenerationParams{ - ReceiptType: "NFT Purchase", // TODO: retrieve dynamically - CustomerName: name, - StringPaymentId: p.transactionModel.Id, - PaymentDescriptor: p.executionRequest.Quote.TransactionRequest.AssetName, - TransactionDate: time.Now().Format(time.RFC1123), - TransactionId: *p.txId, - TransactionExplorer: p.chain.Explorer + "/tx/" + *p.txId, - DestinationAddress: transactionRequest.UserAddress, - DestinationExplorer: p.chain.Explorer + "/address/" + transactionRequest.UserAddress, - PaymentMethod: p.cardAuthorization.Issuer + " " + p.cardAuthorization.Last4, - Platform: platform.Name, - ItemOrdered: p.executionRequest.Quote.TransactionRequest.AssetName, - TokenId: p.tokenIds, - Subtotal: common.FloatToUSDString(estimate.BaseUSD + estimate.TokenUSD), - NetworkFee: common.FloatToUSDString(estimate.GasUSD), - ProcessingFee: common.FloatToUSDString(estimate.ServiceUSD), - Total: common.FloatToUSDString(estimate.TotalUSD), + ReceiptType: "NFT Purchase", // TODO: retrieve dynamically + CustomerName: name, + StringPaymentId: p.transactionModel.Id, + PaymentDescriptor: p.executionRequest.Quote.TransactionRequest.AssetName, + TransactionDate: time.Now().Format(time.RFC1123), + TransactionIds: p.txIds, + TransactionExplorers: explorers, + DestinationAddress: transactionRequest.UserAddress, + DestinationExplorer: p.chain.Explorer + "/address/" + transactionRequest.UserAddress, + PaymentMethod: p.cardAuthorization.Issuer + " " + p.cardAuthorization.Last4, + Platform: platform.Name, + ItemOrdered: p.executionRequest.Quote.TransactionRequest.AssetName, + TokenIds: p.tokenIds, + Subtotal: common.FloatToUSDString(estimate.BaseUSD + estimate.TokenUSD), + NetworkFee: common.FloatToUSDString(estimate.GasUSD), + ProcessingFee: common.FloatToUSDString(estimate.ServiceUSD), + Total: common.FloatToUSDString(estimate.TotalUSD), } emailer := emailer.New() @@ -945,21 +984,25 @@ func (t transaction) isContractAllowed(ctx context.Context, platformId string, n _, finish := Span(ctx, "service.transaction.isContractAllowed", SpanTag{"platformId": platformId}) defer finish() - contract, err := t.repos.Contract.GetByAddressAndNetworkAndPlatform(ctx, request.CxAddr, networkId, platformId) - if err != nil && err == serror.NOT_FOUND { - return false, libcommon.StringError(serror.CONTRACT_NOT_ALLOWED) - } else if err != nil { - return false, libcommon.StringError(err) - } + for _, action := range request.Actions { + cxAddr := action.CxAddr + contract, err := t.repos.Contract.GetByAddressAndNetworkAndPlatform(ctx, cxAddr, networkId, platformId) + if err != nil && err == serror.NOT_FOUND { + return false, libcommon.StringError(serror.CONTRACT_NOT_ALLOWED) + } else if err != nil { + return false, libcommon.StringError(err) + } - if len(contract.Functions) == 0 { - return true, nil - } + if len(contract.Functions) == 0 { + continue + } - for _, function := range contract.Functions { - if function == request.CxFunc { - return true, nil + for _, function := range contract.Functions { + if function == action.CxFunc { + continue + } } } - return false, libcommon.StringError(serror.FUNC_NOT_ALLOWED) + + return true, nil } From 948f4e933c21491a8e1a9bed0dd945ff317aa621 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 6 Jul 2023 20:26:42 -0700 Subject: [PATCH 06/17] improved error message --- pkg/service/cost.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/service/cost.go b/pkg/service/cost.go index 60f9c270..457d5808 100644 --- a/pkg/service/cost.go +++ b/pkg/service/cost.go @@ -166,7 +166,7 @@ func (c cost) EstimateTransaction(p EstimationParams, chain Chain) (estimate mod costTokenEth := common.WeiToEther(&costToken) tokenName, ok := coinMapping[CoinKey{chain.ChainId, p.TokenAddrs[i]}] if !ok { - return estimate, errors.New("token not found") + return estimate, errors.New("CoinGecko does not list token " + p.TokenAddrs[i]) } tokenCost, err := c.LookupUSD(costTokenEth, tokenName) if err != nil { From 3ec87216dd5812e8c52e89c605f4cdb80b69f211 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 6 Jul 2023 20:31:32 -0700 Subject: [PATCH 07/17] sanitize coingecko checksums --- pkg/service/cost.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/service/cost.go b/pkg/service/cost.go index 457d5808..2298fdd2 100644 --- a/pkg/service/cost.go +++ b/pkg/service/cost.go @@ -117,7 +117,7 @@ func GetCoingeckoCoinMapping() (map[CoinKey]string, error) { for key, val := range coin.Platforms { newKey := CoinKey{ ChainId: platform_to_id[key], - Address: val, + Address: common.SanitizeChecksum(val), } coin_key_to_id[newKey] = coin.ID } @@ -164,6 +164,7 @@ func (c cost) EstimateTransaction(p EstimationParams, chain Chain) (estimate mod } for i, costToken := range p.CostTokens { costTokenEth := common.WeiToEther(&costToken) + tokenName, ok := coinMapping[CoinKey{chain.ChainId, p.TokenAddrs[i]}] if !ok { return estimate, errors.New("CoinGecko does not list token " + p.TokenAddrs[i]) From e63222b6945bb010ca6283635c381d42e8743409 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 6 Jul 2023 20:47:26 -0700 Subject: [PATCH 08/17] cull bunk address data from coingecko --- pkg/service/cost.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/service/cost.go b/pkg/service/cost.go index 2298fdd2..1671342e 100644 --- a/pkg/service/cost.go +++ b/pkg/service/cost.go @@ -115,6 +115,10 @@ func GetCoingeckoCoinMapping() (map[CoinKey]string, error) { coin_key_to_id := make(map[CoinKey]string) for _, coin := range coins { for key, val := range coin.Platforms { + // There's some weird data floating around in here. Ignore it. + if len(val) != 42 || val[:2] != "0x" { + continue + } newKey := CoinKey{ ChainId: platform_to_id[key], Address: common.SanitizeChecksum(val), From 4591fb041e7ae64ec3e18ba6e2b905ed7d1a69c9 Mon Sep 17 00:00:00 2001 From: Sean Date: Sun, 9 Jul 2023 18:06:41 -0700 Subject: [PATCH 09/17] added mock cost lookup for String USDc, fixed EVM estimation and execution of multiple calls, added calls for NFT and Token Forwarding (not complete yet) --- pkg/service/cost.go | 8 ++++++++ pkg/service/executor.go | 40 ++++++++++++++++++++++++++------------ pkg/service/transaction.go | 20 +++++++++++++++++-- 3 files changed, 54 insertions(+), 14 deletions(-) diff --git a/pkg/service/cost.go b/pkg/service/cost.go index 1671342e..185ba248 100644 --- a/pkg/service/cost.go +++ b/pkg/service/cost.go @@ -163,10 +163,18 @@ func (c cost) EstimateTransaction(p EstimationParams, chain Chain) (estimate mod // // Query cost of token in USD if used and apply buffer totalTokenCost := 0.0 coinMapping, err := GetCoingeckoCoinMapping() + // Coingecko is going down during testing. Comment this out if needed. if err != nil { return estimate, libcommon.StringError(err) } for i, costToken := range p.CostTokens { + // For testing only, dev fuji usdc is not listed on coingecko + if p.TokenAddrs[i] == "0x671E35F91Cc497385f9f7d0dFCB7192848b1015b" { + costTokenEth := common.WeiToEther(&costToken) + totalTokenCost += 1.0 * costTokenEth + continue + } + costTokenEth := common.WeiToEther(&costToken) tokenName, ok := coinMapping[CoinKey{chain.ChainId, p.TokenAddrs[i]}] diff --git a/pkg/service/executor.go b/pkg/service/executor.go index 3f54fc46..03632995 100644 --- a/pkg/service/executor.go +++ b/pkg/service/executor.go @@ -88,21 +88,29 @@ func (e executor) Estimate(calls []ContractCall) (CallEstimate, error) { w3calls := []w3types.Caller{} estimatedGasses := make([]uint64, len(calls)) totalValue := big.NewInt(0) + includesApprove := false for i, call := range calls { // TODO: Further optimize this to take in the calls array - msg, err := e.generateTransactionMessage(call) + msg, err := e.generateTransactionMessage(call, uint64(i)) if err != nil { return CallEstimate{}, libcommon.StringError(err) } totalValue = totalValue.Add(totalValue, msg.Value) + w3calls = append(w3calls, eth.EstimateGas(&msg, nil).Returns(&estimatedGasses[i])) + + // simulation will not track the state of the blockchain after approval + if strings.Contains(strings.ToLower(strings.ReplaceAll(call.CxFunc, " ", "")), "approve") { + includesApprove = true + } } // Estimate gas of messages err := e.client.Call(w3calls...) - if err != nil { - // Execution Will Revert! + _, ok := err.(w3.CallErrors) + if !includesApprove && (err != nil && ok) { return CallEstimate{Value: *big.NewInt(0), Gas: 0, Success: false}, libcommon.StringError(err) } + // Call(w3calls) should fill out estimatedGasses array totalGas := uint64(0) for _, gas := range estimatedGasses { @@ -117,7 +125,7 @@ func (e executor) Initiate(calls []ContractCall) ([]string, *big.Int, error) { totalValue := big.NewInt(0) for i, call := range calls { // TODO: Further optimize this to take in the calls array - tx, err := e.generateTransactionRequest(call) + tx, err := e.generateTransactionRequest(call, uint64(i)) if err != nil { return []string{}, nil, libcommon.StringError(err) } @@ -127,10 +135,17 @@ func (e executor) Initiate(calls []ContractCall) ([]string, *big.Int, error) { // Call txs and retrieve hashes err := e.client.Call(w3calls...) - if err != nil { - // Execution failed! - return []string{}, nil, libcommon.StringError(err) + callErrs, ok := err.(w3.CallErrors) + if err != nil && ok { + catErrs := "" + for _, callErr := range callErrs { + if callErr != nil { + catErrs += callErr.Error() + " " + } + } + return []string{}, nil, libcommon.StringError(errors.New(catErrs)) } + hashStrings := make([]string, len(hashes)) for i := range hashes { hashStrings[i] = hashes[i].String() @@ -214,7 +229,7 @@ func (e executor) GetBalance() (float64, error) { return fbalance, nil // We like thinking in floats } -func (e executor) generateTransactionMessage(call ContractCall) (w3types.Message, error) { +func (e executor) generateTransactionMessage(call ContractCall, incrementNonce uint64) (w3types.Message, error) { sender, err := e.getAccount() if err != nil { return w3types.Message{}, libcommon.StringError(err) @@ -261,14 +276,14 @@ func (e executor) generateTransactionMessage(call ContractCall) (w3types.Message GasTipCap: tipCap, Value: value, Input: data, - Nonce: nonce, + Nonce: nonce + incrementNonce, }, nil } -func (e executor) generateTransactionRequest(call ContractCall) (types.Transaction, error) { +func (e executor) generateTransactionRequest(call ContractCall, incrementNonce uint64) (types.Transaction, error) { tx := types.Transaction{} - msg, err := e.generateTransactionMessage(call) + msg, err := e.generateTransactionMessage(call, 0) if err != nil { return tx, libcommon.StringError(err) } @@ -293,7 +308,7 @@ func (e executor) generateTransactionRequest(call ContractCall) (types.Transacti // Generate blockchain tx dynamicFeeTx := types.DynamicFeeTx{ ChainID: chainIdBig, - Nonce: msg.Nonce, + Nonce: msg.Nonce + incrementNonce, GasTipCap: tipCap, GasFeeCap: feeCap, Gas: w3.I(call.TxGasLimit).Uint64(), @@ -466,6 +481,7 @@ func (e executor) ForwardTokens(txIds []string, recipient string) ([]string, []s } } + // TODO: use this forwardTxIds, gas, err := e.Initiate(calls) filteredTxIds = append(filteredTxIds, forwardTxIds...) gasUsed = gasUsed.Add(gasUsed, gas) diff --git a/pkg/service/transaction.go b/pkg/service/transaction.go index a9db7e86..203b769b 100644 --- a/pkg/service/transaction.go +++ b/pkg/service/transaction.go @@ -83,6 +83,7 @@ type transactionProcessingData struct { cumulativeValue *big.Int trueGas *uint64 tokenIds string + tokenQuantities string } func (t transaction) Quote(ctx context.Context, d model.TransactionRequest, platformId string) (res model.Quote, err error) { @@ -428,7 +429,7 @@ func (t transaction) postProcess(ctx context.Context, p transactionProcessingDat // TODO: Handle error instead of returning it } - // Get the Token IDs which were transferred + // Get the Token IDs which were transferred to the API tokenIds, err := executor.GetTokenIds(p.txIds) if err != nil { log.Err(err).Msg("Failed to get token ids") @@ -436,13 +437,28 @@ func (t transaction) postProcess(ctx context.Context, p transactionProcessingDat } p.tokenIds = strings.Join(tokenIds, ",") - // Forward any tokens received to the user + // 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) + } + + // Get the Token quantities which were transferred to the API + 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) } + // TODO: Get the final gas total here and cache it to the quote cache. And use it for subsequent quotes. + // We can close the executor because we aren't using it after this executor.Close() From 2c7ef8cfc55e1eba46f7b554d3bc5c7cf92efaf0 Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 11 Jul 2023 13:58:40 -0700 Subject: [PATCH 10/17] stashing --- pkg/service/cost.go | 45 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/pkg/service/cost.go b/pkg/service/cost.go index 185ba248..8ffe8c20 100644 --- a/pkg/service/cost.go +++ b/pkg/service/cost.go @@ -1,6 +1,7 @@ package service import ( + "fmt" "math" "math/big" "strconv" @@ -55,8 +56,13 @@ type CoingeckoCoin struct { } type CoinKey struct { - ChainId uint64 - Address string + ChainId uint64 `json:"chainId"` + Address string `json:"address"` +} + +type CoingeckoMapCache struct { + Timestamp int64 `json:"timestamp"` + Value map[CoinKey]string `json:"value"` } type CostCache struct { @@ -130,6 +136,34 @@ func GetCoingeckoCoinMapping() (map[CoinKey]string, error) { return coin_key_to_id, nil } +// TODO: This logic is being reused, abstract it by templating and refactor +// i.e. LookupCache(cacheName string, rateLimit float, updateMethod func() (T, error)) (T, error) +func (c cost) LookupCoingeckoMapping() (map[CoinKey]string, error) { + cacheName := "coingecko_mapping" + cacheObject, err := store.GetObjectFromCache[CoingeckoMapCache](c.redis, cacheName) + if err != nil { + return map[CoinKey]string{}, libcommon.StringError(err) + } + if len(cacheObject.Value) == 0 || time.Now().Unix()-cacheObject.Timestamp > c.getExternalAPICallInterval(0.004, 1) { + updatedObject := CoingeckoMapCache{} + updatedObject.Timestamp = time.Now().Unix() + updatedObject.Value, err = GetCoingeckoCoinMapping() + // If update fails, return the old object + if err != nil { + return cacheObject.Value, libcommon.StringError(err) + } + err = store.PutObjectInCache(c.redis, cacheName, updatedObject) + // If store fails, return the new object anyway + if err != nil { + return updatedObject.Value, libcommon.StringError(err) + } + cacheObject = updatedObject + } + + // Return the old or new object + return cacheObject.Value, nil +} + func (c cost) EstimateTransaction(p EstimationParams, chain Chain) (estimate model.Estimate[float64], err error) { // Get Unix Timestamp and chain info timestamp := time.Now().Unix() @@ -162,10 +196,13 @@ func (c cost) EstimateTransaction(p EstimationParams, chain Chain) (estimate mod // // Query cost of token in USD if used and apply buffer totalTokenCost := 0.0 - coinMapping, err := GetCoingeckoCoinMapping() + coinMapping, err := c.LookupCoingeckoMapping() // Coingecko is going down during testing. Comment this out if needed. - if err != nil { + if err != nil && len(coinMapping) == 0 { return estimate, libcommon.StringError(err) + } else if err != nil { + // TODO: Log error and continue + fmt.Printf("LookupCoingeckoMapping failed: %s", err) } for i, costToken := range p.CostTokens { // For testing only, dev fuji usdc is not listed on coingecko From 4a3a5b64128829789c716308cc52af79e38b460d Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 11 Jul 2023 14:10:13 -0700 Subject: [PATCH 11/17] Fix Coingecko Data Caching --- pkg/service/cost.go | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/pkg/service/cost.go b/pkg/service/cost.go index 8ffe8c20..313949eb 100644 --- a/pkg/service/cost.go +++ b/pkg/service/cost.go @@ -60,9 +60,13 @@ type CoinKey struct { Address string `json:"address"` } +func (c CoinKey) String() string { + return fmt.Sprintf("%d:%s", c.ChainId, c.Address) +} + type CoingeckoMapCache struct { - Timestamp int64 `json:"timestamp"` - Value map[CoinKey]string `json:"value"` + Timestamp int64 `json:"timestamp"` + Value map[string]string `json:"value"` } type CostCache struct { @@ -105,20 +109,20 @@ func GetCoingeckoPlatformMapping() (map[uint64]string, map[string]uint64, error) return id_to_platform, platform_to_id, nil } -func GetCoingeckoCoinMapping() (map[CoinKey]string, error) { +func GetCoingeckoCoinMapping() (map[string]string, error) { _, platform_to_id, err := GetCoingeckoPlatformMapping() if err != nil { - return map[CoinKey]string{}, libcommon.StringError(err) + return map[string]string{}, libcommon.StringError(err) } // get list of platform names from coingecko to create mapping to chainid var coins []CoingeckoCoin err = common.GetJsonGeneric("https://api.coingecko.com/api/v3/coins/list?include_platform=true", &coins) if err != nil { - return map[CoinKey]string{}, libcommon.StringError(err) + return map[string]string{}, libcommon.StringError(err) } - coin_key_to_id := make(map[CoinKey]string) + coin_key_to_id := 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. @@ -128,7 +132,7 @@ func GetCoingeckoCoinMapping() (map[CoinKey]string, error) { newKey := CoinKey{ ChainId: platform_to_id[key], Address: common.SanitizeChecksum(val), - } + }.String() coin_key_to_id[newKey] = coin.ID } } @@ -138,11 +142,11 @@ func GetCoingeckoCoinMapping() (map[CoinKey]string, error) { // TODO: This logic is being reused, abstract it by templating and refactor // i.e. LookupCache(cacheName string, rateLimit float, updateMethod func() (T, error)) (T, error) -func (c cost) LookupCoingeckoMapping() (map[CoinKey]string, error) { +func (c cost) LookupCoingeckoMapping() (map[string]string, error) { cacheName := "coingecko_mapping" cacheObject, err := store.GetObjectFromCache[CoingeckoMapCache](c.redis, cacheName) if err != nil { - return map[CoinKey]string{}, libcommon.StringError(err) + return map[string]string{}, libcommon.StringError(err) } if len(cacheObject.Value) == 0 || time.Now().Unix()-cacheObject.Timestamp > c.getExternalAPICallInterval(0.004, 1) { updatedObject := CoingeckoMapCache{} @@ -214,7 +218,7 @@ func (c cost) EstimateTransaction(p EstimationParams, chain Chain) (estimate mod costTokenEth := common.WeiToEther(&costToken) - tokenName, ok := coinMapping[CoinKey{chain.ChainId, p.TokenAddrs[i]}] + tokenName, ok := coinMapping[CoinKey{chain.ChainId, p.TokenAddrs[i]}.String()] if !ok { return estimate, errors.New("CoinGecko does not list token " + p.TokenAddrs[i]) } From caf8ca832a71de7e69c2be97c7b86a487721da29 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 12 Jul 2023 14:50:32 -0700 Subject: [PATCH 12/17] Added subnet token proxies quick-fix / hack, fixed a bug where coingecko always appeared to be down, fixed an issue with updating the timestamp of a cached token value even when failing to retrieve a new value --- pkg/service/cost.go | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/pkg/service/cost.go b/pkg/service/cost.go index 313949eb..4dd16fbe 100644 --- a/pkg/service/cost.go +++ b/pkg/service/cost.go @@ -80,12 +80,21 @@ type Cost interface { } type cost struct { - redis database.RedisStore // cached token and gas costs + redis database.RedisStore // cached token and gas costs + subnetTokenProxies map[CoinKey]CoinKey } func NewCost(redis database.RedisStore) Cost { + // Temporarily hard-coding this to reduce future cost-of-change with database + subnetTokenProxies := map[CoinKey]CoinKey{ + // USDc DFK Subnet -> USDc Avalanche: + {ChainId: 53935, Address: "0x3AD9DFE640E1A9Cc1D9B0948620820D975c3803a"}: {ChainId: 43114, Address: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E"}, + // String USDc Fuji Testnet -> USDc Avalanche + {ChainId: 43113, Address: "0x671E35F91Cc497385f9f7d0dFCB7192848b1015b"}: {ChainId: 43114, Address: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E"}, + } return &cost{ - redis: redis, + redis: redis, + subnetTokenProxies: subnetTokenProxies, } } @@ -209,16 +218,16 @@ func (c cost) EstimateTransaction(p EstimationParams, chain Chain) (estimate mod fmt.Printf("LookupCoingeckoMapping failed: %s", err) } for i, costToken := range p.CostTokens { - // For testing only, dev fuji usdc is not listed on coingecko - if p.TokenAddrs[i] == "0x671E35F91Cc497385f9f7d0dFCB7192848b1015b" { - costTokenEth := common.WeiToEther(&costToken) - totalTokenCost += 1.0 * costTokenEth - continue + // TODO: Get subnetTokenProxies from the database + coinKey := CoinKey{chain.ChainId, p.TokenAddrs[i]} + proxy := c.subnetTokenProxies[CoinKey{chain.ChainId, p.TokenAddrs[i]}] + if proxy.Address != "" { + coinKey = proxy } costTokenEth := common.WeiToEther(&costToken) - tokenName, ok := coinMapping[CoinKey{chain.ChainId, p.TokenAddrs[i]}.String()] + tokenName, ok := coinMapping[coinKey.String()] if !ok { return estimate, errors.New("CoinGecko does not list token " + p.TokenAddrs[i]) } @@ -288,16 +297,20 @@ func (c cost) LookupUSD(quantity float64, coins ...string) (float64, error) { return 0.0, libcommon.StringError(err) } if cacheObject == (CostCache{}) || (err == nil && time.Now().Unix()-cacheObject.Timestamp > c.getExternalAPICallInterval(10, 6)) { - cacheObject.Timestamp = time.Now().Unix() // If coingecko is down, use coincap to get the price var empty interface{} - err = common.GetJson(config.Var.COINGECKO_API_URL+"ping", &empty) + err = common.GetJsonGeneric(config.Var.COINGECKO_API_URL+"ping", &empty) if err == nil { + // Only update timestamp if we reacquire the value + cacheObject.Timestamp = time.Now().Unix() cacheObject.Value, err = c.coingeckoUSD(coins[0]) if err != nil { return 0, libcommon.StringError(err) } - } else if len(coins) > 1 { + + } else if len(coins) > 1 && coins[1] != "" { + // Only update the timestamp if we reacquire the value + cacheObject.Timestamp = time.Now().Unix() cacheObject.Value, err = c.coincapUSD(coins[1]) if err != nil { @@ -310,6 +323,7 @@ func (c cost) LookupUSD(quantity float64, coins ...string) (float64, error) { } } + // If both services are down, use the last value we had return cacheObject.Value * quantity, nil } From 5c0a237c67e482c1a0c060aeab0c07fce37ebe37 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 12 Jul 2023 14:57:34 -0700 Subject: [PATCH 13/17] Refactor call to repos.Contract.GetForValidation after resolving merge conflicts --- 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 f9a7ae3c..d4b980ae 100644 --- a/pkg/service/transaction.go +++ b/pkg/service/transaction.go @@ -1005,7 +1005,7 @@ func (t transaction) isContractAllowed(ctx context.Context, platformId string, n for _, action := range request.Actions { cxAddr := action.CxAddr - contract, err := t.repos.Contract.GetByAddressAndNetworkAndPlatform(ctx, cxAddr, networkId, platformId) + 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) } else if err != nil { From b8532726ca6cb802383507cbcc2ac6944ce66080 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 12 Jul 2023 17:09:50 -0700 Subject: [PATCH 14/17] fix unit21 error from giving it comma delineated txIds --- pkg/service/transaction.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/service/transaction.go b/pkg/service/transaction.go index d4b980ae..f3a16590 100644 --- a/pkg/service/transaction.go +++ b/pkg/service/transaction.go @@ -460,6 +460,13 @@ func (t transaction) postProcess(ctx context.Context, p transactionProcessingDat executor.ForwardTokens(p.txIds, p.executionRequest.Quote.TransactionRequest.UserAddress) } + // Cull any TXIDs from Approve(), the user and Unit21 and the receipt don't need them + for i, action := range p.executionRequest.Quote.TransactionRequest.Actions { + if strings.Contains(strings.ToLower(action.CxFunc), "approve") { + p.txIds = append(p.txIds[:i], p.txIds[i+1:]...) + } + } + // TODO: Get the final gas total here and cache it to the quote cache. And use it for subsequent quotes. // We can close the executor because we aren't using it after this From 7e27be0f28610c4541cf6bf93de98cd3efa09458 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 12 Jul 2023 17:23:22 -0700 Subject: [PATCH 15/17] Removed TX IDs from approvals from all front-facing stuff (receipt, transact return body) and unit21 --- pkg/internal/emailer/emailer.go | 34 +++++++++++++------------- pkg/service/transaction.go | 43 ++++++++++++++++++--------------- 2 files changed, 40 insertions(+), 37 deletions(-) diff --git a/pkg/internal/emailer/emailer.go b/pkg/internal/emailer/emailer.go index 418827bf..8ac44ddd 100644 --- a/pkg/internal/emailer/emailer.go +++ b/pkg/internal/emailer/emailer.go @@ -29,23 +29,23 @@ func New() Emailer { } type ReceiptGenerationParams struct { - ReceiptType string - CustomerName string - PaymentDescriptor string - TransactionDate string - StringPaymentId string - TransactionIds []string - TransactionExplorers []string - DestinationAddress string - DestinationExplorer string - PaymentMethod string - Platform string - ItemOrdered string - TokenIds string - Subtotal string - NetworkFee string - ProcessingFee string - Total string + ReceiptType string + CustomerName string + PaymentDescriptor string + TransactionDate string + StringPaymentId string + TransactionId string + TransactionExplorer string + DestinationAddress string + DestinationExplorer string + PaymentMethod string + Platform string + ItemOrdered string + TokenIds string + Subtotal string + NetworkFee string + ProcessingFee string + Total string } func (e emailer) SendEmailVerification(ctx context.Context, email string, code string, platformName string) error { diff --git a/pkg/service/transaction.go b/pkg/service/transaction.go index f3a16590..085c1449 100644 --- a/pkg/service/transaction.go +++ b/pkg/service/transaction.go @@ -166,9 +166,12 @@ func (t transaction) Execute(ctx context.Context, e model.ExecutionRequest, user ids := []string{} urls := []string{} - for _, id := range p.txIds { - ids = append(ids, id) - urls = append(urls, p.chain.Explorer+"/tx/"+id) + for i, id := range p.txIds { + // check if lowercase version of p.executionRequest.Quote.TransactionRequest.Actions[i].CxFunc contains the word "approve" + if !strings.Contains(strings.ToLower(p.executionRequest.Quote.TransactionRequest.Actions[i].CxFunc), "approve") { + ids = append(ids, id) + urls = append(urls, p.chain.Explorer+"/tx/"+id) + } } return model.TransactionReceipt{TxIds: ids, TxURLs: urls, TxTimestamp: time.Now().Format(time.RFC1123)}, nil } @@ -937,23 +940,23 @@ func (t transaction) sendEmailReceipt(ctx context.Context, p transactionProcessi } receiptParams := emailer.ReceiptGenerationParams{ - ReceiptType: "NFT Purchase", // TODO: retrieve dynamically - CustomerName: name, - StringPaymentId: p.transactionModel.Id, - PaymentDescriptor: p.executionRequest.Quote.TransactionRequest.AssetName, - TransactionDate: time.Now().Format(time.RFC1123), - TransactionIds: p.txIds, - TransactionExplorers: explorers, - DestinationAddress: transactionRequest.UserAddress, - DestinationExplorer: p.chain.Explorer + "/address/" + transactionRequest.UserAddress, - PaymentMethod: p.cardAuthorization.Issuer + " " + p.cardAuthorization.Last4, - Platform: platform.Name, - ItemOrdered: p.executionRequest.Quote.TransactionRequest.AssetName, - TokenIds: p.tokenIds, - Subtotal: common.FloatToUSDString(estimate.BaseUSD + estimate.TokenUSD), - NetworkFee: common.FloatToUSDString(estimate.GasUSD), - ProcessingFee: common.FloatToUSDString(estimate.ServiceUSD), - Total: common.FloatToUSDString(estimate.TotalUSD), + ReceiptType: "NFT Purchase", // TODO: retrieve dynamically + CustomerName: name, + StringPaymentId: p.transactionModel.Id, + PaymentDescriptor: p.executionRequest.Quote.TransactionRequest.AssetName, + TransactionDate: time.Now().Format(time.RFC1123), + TransactionId: p.txIds[0], // For now assume there were 2 and the approval one was removed + TransactionExplorer: explorers[0], // For now assume there were 2 and the approval one was removed + DestinationAddress: transactionRequest.UserAddress, + DestinationExplorer: p.chain.Explorer + "/address/" + transactionRequest.UserAddress, + PaymentMethod: p.cardAuthorization.Issuer + " " + p.cardAuthorization.Last4, + Platform: platform.Name, + ItemOrdered: p.executionRequest.Quote.TransactionRequest.AssetName, + TokenIds: p.tokenIds, + Subtotal: common.FloatToUSDString(estimate.BaseUSD + estimate.TokenUSD), + NetworkFee: common.FloatToUSDString(estimate.GasUSD), + ProcessingFee: common.FloatToUSDString(estimate.ServiceUSD), + Total: common.FloatToUSDString(estimate.TotalUSD), } emailer := emailer.New() From d16178e056f2be82e37bb67b1194df973b2c22b6 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 12 Jul 2023 17:42:40 -0700 Subject: [PATCH 16/17] Don't dereference nil in code which isn't doing anything yet --- pkg/service/executor.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pkg/service/executor.go b/pkg/service/executor.go index 03632995..6dd62a74 100644 --- a/pkg/service/executor.go +++ b/pkg/service/executor.go @@ -482,9 +482,14 @@ func (e executor) ForwardTokens(txIds []string, recipient string) ([]string, []s } // TODO: use this - forwardTxIds, gas, err := e.Initiate(calls) - filteredTxIds = append(filteredTxIds, forwardTxIds...) - gasUsed = gasUsed.Add(gasUsed, gas) + if len(calls) > 0 { + forwardTxIds, gas, 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 } From abed33a6ff1d592aa6208a9db8141569e649d8a1 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 12 Jul 2023 17:54:35 -0700 Subject: [PATCH 17/17] Cull Approve txIds where needed --- pkg/internal/emailer/emailer.go | 2 +- pkg/service/transaction.go | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pkg/internal/emailer/emailer.go b/pkg/internal/emailer/emailer.go index 8ac44ddd..92bc9a75 100644 --- a/pkg/internal/emailer/emailer.go +++ b/pkg/internal/emailer/emailer.go @@ -41,7 +41,7 @@ type ReceiptGenerationParams struct { PaymentMethod string Platform string ItemOrdered string - TokenIds string + TokenId string Subtotal string NetworkFee string ProcessingFee string diff --git a/pkg/service/transaction.go b/pkg/service/transaction.go index 085c1449..830a4545 100644 --- a/pkg/service/transaction.go +++ b/pkg/service/transaction.go @@ -383,7 +383,15 @@ func (t transaction) initiateTransaction(ctx context.Context, p transactionProce status := "Transaction Initiated" txAmount := p.cumulativeValue.String() - hashes := strings.Join(p.txIds, ", ") + + hashesOtherThanApprove := []string{} + for i, txId := range p.txIds { + if !strings.Contains(strings.ToLower(p.executionRequest.Quote.TransactionRequest.Actions[i].CxFunc), "approve") { + hashesOtherThanApprove = append(hashesOtherThanApprove, txId) + } + } + + hashes := strings.Join(hashesOtherThanApprove, ", ") updateDB := &model.TransactionUpdates{Status: &status, TransactionHash: &hashes, TransactionAmount: &txAmount} err = t.repos.Transaction.Update(ctx, p.transactionModel.Id, updateDB) if err != nil { @@ -952,7 +960,7 @@ func (t transaction) sendEmailReceipt(ctx context.Context, p transactionProcessi PaymentMethod: p.cardAuthorization.Issuer + " " + p.cardAuthorization.Last4, Platform: platform.Name, ItemOrdered: p.executionRequest.Quote.TransactionRequest.AssetName, - TokenIds: p.tokenIds, + TokenId: p.tokenIds, Subtotal: common.FloatToUSDString(estimate.BaseUSD + estimate.TokenUSD), NetworkFee: common.FloatToUSDString(estimate.GasUSD), ProcessingFee: common.FloatToUSDString(estimate.ServiceUSD),