Skip to content

Swap! #98

New issue

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

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

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: develop
Choose a base branch
from
Draft
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ run `go install` to get dependencies installed
### For local testing: ###
run `go test`

### To run an individual test in verbose mode ###
`go test -run [TestName] [TestDirectory] -v -count 1`
i.e. `go test -run TestGetSwapPayload ./pkg/service -v -count 1`

### Unit21: ###
This is a 3rd party service that offers the ability to evaluate risk at a transaction level and identify fraud. A client file exists to connect to their API. Documentation is here: https://docs.unit21.ai/reference/entities-api
You can create a test API key on the Unit21 dashboard. You will need to be setup as an Admin. Here are the instructions: https://docs.unit21.ai/reference/generate-api-keys
Expand Down
18 changes: 18 additions & 0 deletions api/handler/quotes.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

type Quotes interface {
Quote(c echo.Context) error
OffRampQuote(c echo.Context) error
RegisterRoutes(g *echo.Group, ms ...echo.MiddlewareFunc)
}

Expand Down Expand Up @@ -47,11 +48,28 @@ func (q quote) Quote(c echo.Context) error {
return c.JSON(http.StatusOK, res)
}

func (q quote) OffRampQuote(c echo.Context) error {
var body model.OffRampRequest
err := c.Bind(&body)
if err != nil {
LogStringError(c, err, "quote: offrampquote bind")
return BadRequestError(c)
}
SanitizeChecksums(&body.FromToken, &body.UserAddress)
res, err := q.Service.OffRampQuote(body)
if err != nil {
LogStringError(c, err, "quote: offrampquote")
return c.JSON(http.StatusInternalServerError, JSONError{Message: "Quote Service Failed"})
}
return c.JSON(http.StatusOK, res)
}

func (q quote) RegisterRoutes(g *echo.Group, ms ...echo.MiddlewareFunc) {
if g == nil {
panic("No group attached to the Quote Handler")
}
q.Group = g
g.Use(ms...)
g.POST("", q.Quote)
g.POST("/offramp", q.OffRampQuote)
}
32 changes: 32 additions & 0 deletions pkg/model/swap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package model

type OffRampRequest struct {
UserAddress string `json:"userAddress"`
ChainID int `json:"chainID"`
FromToken string `json:"fromToken"`
Amount string `json:"amount"`
}

type OffRampExecutionRequest struct {
OffRampRequest
Quote
Signature string `json:"signature"`
CardToken string `json:"cardToken"`
}

type SwapRequest struct {
UserAddress string `json:"userAddress"`
ChainID int `json:"chainID"`
FromToken string `json:"fromToken"`
ToToken string `json:"toToken"`
Amount string `json:"amount"`
}

type SwapCrossChainRequest struct {
UserAddress string `json:"userAddress"`
FromChain int `json:"fromChain"`
ToChain int `json:"toChain"`
FromToken string `json:"fromToken"`
ToToken string `json:"toToken"`
Amount string `json:"amount"`
}
5 changes: 3 additions & 2 deletions pkg/service/cost.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type CostCache struct {
type Cost interface {
EstimateTransaction(p EstimationParams, chain Chain) (model.Quote, error)
LookupUSD(coin string, quantity float64) (float64, error)
CoingeckoUSD(coin string, quantity float64) (float64, error)
}

type cost struct {
Expand Down Expand Up @@ -135,7 +136,7 @@ func (c cost) LookupUSD(coin string, quantity float64) (float64, error) {
}
if cacheObject == (CostCache{}) || (err == nil && time.Now().Unix()-cacheObject.Timestamp > c.getExternalAPICallInterval(10, 6)) {
cacheObject.Timestamp = time.Now().Unix()
cacheObject.Value, err = c.coingeckoUSD(coin, 1)
cacheObject.Value, err = c.CoingeckoUSD(coin, 1)
if err != nil {
return 0, common.StringError(err)
}
Expand Down Expand Up @@ -169,7 +170,7 @@ func (c cost) lookupGas(network string) (float64, error) {
return cacheObject.Value, nil
}

func (c cost) coingeckoUSD(coin string, quantity float64) (float64, error) {
func (c cost) CoingeckoUSD(coin string, quantity float64) (float64, error) {
requestURL := os.Getenv("COINGECKO_API_URL") + "simple/price?ids=" + coin + "&vs_currencies=usd"
var res map[string]interface{}
err := common.GetJsonGeneric(requestURL, &res)
Expand Down
85 changes: 85 additions & 0 deletions pkg/service/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ type ContractCall struct {
TxGasLimit string // Gwei gas limit ie "210000 gwei"
}

type EncodedContractCall struct {
Data string // Hex encoded function, parameters etc
From string // Address
Gas string
GasPrice big.Int
To string // Address
Value string
}

type CallEstimate struct {
Value big.Int
Gas uint64
Expand All @@ -36,6 +45,7 @@ type CallEstimate struct {
type Executor interface {
Initialize(RPC string) error
Initiate(call ContractCall) (string, *big.Int, error)
InitiateEncoded(call EncodedContractCall) (string, *big.Int, error)
Estimate(call ContractCall) (CallEstimate, error)
TxWait(txID string) (uint64, error)
Close() error
Expand Down Expand Up @@ -145,6 +155,81 @@ func (e executor) Estimate(call ContractCall) (CallEstimate, error) {
return CallEstimate{Value: *value, Gas: estimatedGas, Success: true}, nil
}

func (e executor) InitiateEncoded(call EncodedContractCall) (string, *big.Int, error) {
// Get private key
skStr, err := stringCommon.DecryptBlobFromKMS(os.Getenv("EVM_PRIVATE_KEY"))
if err != nil {
return "", nil, stringCommon.StringError(err)
}
sk, err := crypto.ToECDSA(common.FromHex(skStr))
if err != nil {
return "", nil, stringCommon.StringError(err)
}
// TODO: avoid panicking so that we get an intelligible error message
to := w3.A(call.To)
value := w3.I(call.Value)
// Get public key
publicKeyECDSA, ok := sk.Public().(*ecdsa.PublicKey)
if !ok {
return "", nil, stringCommon.StringError(errors.New("Estimate: Error casting public key to ECDSA"))
}
sender := crypto.PubkeyToAddress(*publicKeyECDSA)

// Use provided gas limit
gasLimit := w3.I(call.Gas)

// Get chainID from state
var chainId64 uint64
err = e.client.Call(eth.ChainID().Returns(&chainId64))
if err != nil {
return "", nil, stringCommon.StringError(err)
}

// Get sender nonce
var nonce uint64
err = e.client.Call(eth.Nonce(sender, nil).Returns(&nonce))
if err != nil {
return "", nil, stringCommon.StringError(err)
}

// // Get dynamic fee tx gas params
tipCap, _ := e.geth.SuggestGasTipCap(context.Background())
feeCap, _ := e.geth.SuggestGasPrice(context.Background())

// // Function is already encoded along with parameters
// data := []byte(call.Data)
data := w3.B(call.Data)

// Type conversion for chainID
chainIdBig := new(big.Int).SetUint64(chainId64)

// Get signer type, this is used to encode the tx
signer := types.LatestSignerForChainID(chainIdBig)

// Generate blockchain tx
dynamicFeeTx := types.DynamicFeeTx{
ChainID: chainIdBig,
Nonce: nonce,
GasTipCap: tipCap,
GasFeeCap: feeCap,
Gas: gasLimit.Uint64(),
To: &to,
Value: value,
Data: data,
}
// Sign it
tx := types.MustSignNewTx(sk, signer, &dynamicFeeTx)

// Call tx and retrieve hash
var hash common.Hash
err = e.client.Call(eth.SendTx(tx).Returns(&hash))
if err != nil {
// Execution failed!
return "", nil, stringCommon.StringError(err)
}
return hash.String(), value, nil
}

func (e executor) Initiate(call ContractCall) (string, *big.Int, error) {
// Get private key
skStr, err := stringCommon.DecryptBlobFromKMS(os.Getenv("EVM_PRIVATE_KEY"))
Expand Down
Loading