Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions api/handler/quotes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 7 additions & 4 deletions api/handler/transact.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
14 changes: 14 additions & 0 deletions pkg/internal/common/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
28 changes: 16 additions & 12 deletions pkg/model/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,23 @@ 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 {
TxId string `json:"txId"`
TxURL string `json:"txUrl"`
TxTimestamp string `json:"txTimestamp"`
TxIds []string `json:"txIds"`
TxURLs []string `json:"txUrls"`
TxTimestamp string `json:"txTimestamp"`
}
190 changes: 167 additions & 23 deletions pkg/service/cost.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package service

import (
"fmt"
"math"
"math/big"
"strconv"
Expand All @@ -17,12 +18,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 {
Expand All @@ -40,6 +41,34 @@ 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 `json:"chainId"`
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[string]string `json:"value"`
}

type CostCache struct {
Timestamp int64 `json:"timestamp"`
Value float64 `json:"value"`
Expand All @@ -51,13 +80,101 @@ 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,
}
}

// 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[string]string, error) {
_, platform_to_id, err := GetCoingeckoPlatformMapping()
if err != nil {
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[string]string{}, libcommon.StringError(err)
}

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.
if len(val) != 42 || val[:2] != "0x" {
continue
}
newKey := CoinKey{
ChainId: platform_to_id[key],
Address: common.SanitizeChecksum(val),
}.String()
coin_key_to_id[newKey] = coin.ID
}
}

return coin_key_to_id, nil
}

// TODO: This logic is being reused, abstract it by templating and refactor
// i.e. LookupCache<T any>(cacheName string, rateLimit float, updateMethod func() (T, error)) (T, 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[string]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) {
Expand Down Expand Up @@ -90,22 +207,44 @@ 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)
if err != nil {
// // Query cost of token in USD if used and apply buffer
totalTokenCost := 0.0
coinMapping, err := c.LookupCoingeckoMapping()
// Coingecko is going down during testing. Comment this out if needed.
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)
}
if p.UseBuffer {
tokenCost *= 1.0 + common.TokenBuffer(p.TokenName)
for i, costToken := range p.CostTokens {
// 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.String()]
if !ok {
return estimate, errors.New("CoinGecko does not list token " + p.TokenAddrs[i])
}
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 {
Expand All @@ -118,11 +257,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)
Expand All @@ -132,7 +271,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
Expand All @@ -158,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 {
Expand All @@ -180,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
}

Expand Down
15 changes: 15 additions & 0 deletions pkg/service/cost_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading