-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add: requests to estimate and transfer ERC20; update: configs with pr…
…ice api and contracts;
- Loading branch information
Showing
10 changed files
with
576 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
package config | ||
|
||
import ( | ||
"encoding/json" | ||
"io" | ||
"math/big" | ||
"net/http" | ||
"net/url" | ||
"time" | ||
|
||
"gitlab.com/distributed_lab/figure/v3" | ||
"gitlab.com/distributed_lab/kit/comfig" | ||
"gitlab.com/distributed_lab/kit/kv" | ||
"gitlab.com/distributed_lab/logan/v3" | ||
"gitlab.com/distributed_lab/logan/v3/errors" | ||
) | ||
|
||
const priceApiYamlKey = "price_api" | ||
|
||
var ( | ||
ErrPriceApiRequestFailed = errors.New("failed to fetch price api") | ||
ErrEmptyPrice = errors.New("dollar price in ETH is empty") | ||
) | ||
|
||
type PriceApiConfiger interface { | ||
PriceApiConfig() PriceApiConfig | ||
} | ||
|
||
type PriceApiConfig struct { | ||
URL *url.URL `fig:"url,required"` | ||
Key string `fig:"key,required"` | ||
CurrencyId string `fig:"currency_id,required"` | ||
QuoteTag string `fig:"quote_tag,required"` | ||
} | ||
|
||
type priceApi struct { | ||
once comfig.Once | ||
getter kv.Getter | ||
} | ||
|
||
func NewPriceApiConfiger(getter kv.Getter) PriceApiConfiger { | ||
return &priceApi{ | ||
getter: getter, | ||
} | ||
} | ||
|
||
func (v *priceApi) PriceApiConfig() PriceApiConfig { | ||
return v.once.Do(func() interface{} { | ||
var result PriceApiConfig | ||
|
||
err := figure. | ||
Out(&result). | ||
With(figure.BaseHooks). | ||
From(kv.MustGetStringMap(v.getter, priceApiYamlKey)). | ||
Please() | ||
if err != nil { | ||
panic(errors.Wrap(err, "failed to figure out config", logan.F{ | ||
"yaml_key": priceApiYamlKey, | ||
})) | ||
} | ||
|
||
return result | ||
}).(PriceApiConfig) | ||
} | ||
|
||
type QuoteResponse struct { | ||
Data map[string]Currency `json:"data"` | ||
} | ||
|
||
type Currency struct { | ||
Id int `json:"id"` | ||
Name string `json:"name"` | ||
Symbol string `json:"symbol"` | ||
Quote map[string]Quote `json:"quote"` | ||
} | ||
|
||
type Quote struct { | ||
Price float64 `json:"price"` | ||
LastUpdated time.Time `json:"last_updated"` | ||
} | ||
|
||
// ConvertPrice converts tokens price | ||
func (cfg PriceApiConfig) ConvertPrice() (*big.Float, error) { | ||
URL := cfg.URL.JoinPath("/v2/cryptocurrency/quotes/latest") | ||
|
||
query := URL.Query() | ||
query.Set("id", cfg.CurrencyId) | ||
query.Set("convert", cfg.QuoteTag) | ||
|
||
URL.RawQuery = query.Encode() | ||
|
||
request, err := http.NewRequest(http.MethodGet, URL.String(), nil) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "failed to create request", logan.F{ | ||
"url": URL, | ||
}) | ||
} | ||
|
||
request.Header.Set("X-CMC_PRO_API_KEY", cfg.Key) | ||
|
||
response, err := http.DefaultClient.Do(request) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "failed to do request") | ||
} | ||
|
||
if response.StatusCode != http.StatusOK { | ||
body, err := io.ReadAll(response.Body) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "failed to read response body") | ||
} | ||
|
||
return nil, errors.From(ErrPriceApiRequestFailed, logan.F{ | ||
"status": response.StatusCode, | ||
"body": string(body), | ||
}) | ||
} | ||
|
||
var body QuoteResponse | ||
if err = json.NewDecoder(response.Body).Decode(&body); err != nil { | ||
return nil, errors.Wrap(err, "failed to decode response body") | ||
} | ||
|
||
dollarInEth := big.NewFloat(body.Data[cfg.CurrencyId].Quote[cfg.QuoteTag].Price) | ||
if dollarInEth.Cmp(big.NewFloat(0)) == 0 { | ||
return nil, ErrEmptyPrice | ||
} | ||
|
||
return dollarInEth, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package handlers | ||
|
||
import ( | ||
"net/http" | ||
|
||
"github.com/rarimo/evm-airdrop-svc/internal/data" | ||
"github.com/rarimo/evm-airdrop-svc/internal/service/api" | ||
"github.com/rarimo/evm-airdrop-svc/internal/service/api/models" | ||
"github.com/rarimo/evm-airdrop-svc/internal/service/api/requests" | ||
"gitlab.com/distributed_lab/ape" | ||
"gitlab.com/distributed_lab/ape/problems" | ||
) | ||
|
||
func GetAirdrop(w http.ResponseWriter, r *http.Request) { | ||
nullifier, err := requests.NewGetAirdrop(r) | ||
if err != nil { | ||
ape.RenderErr(w, problems.BadRequest(err)...) | ||
return | ||
} | ||
|
||
airdrops, err := api.AirdropsQ(r). | ||
FilterByNullifier(nullifier). | ||
Select() | ||
if err != nil { | ||
api.Log(r).WithError(err).Error("Failed to select airdrops by nullifier") | ||
ape.RenderErr(w, problems.InternalError()) | ||
return | ||
} | ||
if len(airdrops) == 0 { | ||
ape.RenderErr(w, problems.NotFound()) | ||
return | ||
} | ||
|
||
airdrop := airdrops[0] | ||
for _, a := range airdrops[1:] { | ||
if a.Status == data.TxStatusCompleted { | ||
airdrop = a | ||
break | ||
} | ||
} | ||
|
||
ape.Render(w, models.NewAirdropResponse(airdrop)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
package handlers | ||
|
||
import ( | ||
"math/big" | ||
"net/http" | ||
|
||
"github.com/ethereum/go-ethereum/accounts/abi/bind" | ||
"github.com/ethereum/go-ethereum/common/hexutil" | ||
"github.com/ethereum/go-ethereum/core/types" | ||
validation "github.com/go-ozzo/ozzo-validation/v4" | ||
pkgErrors "github.com/pkg/errors" | ||
"github.com/rarimo/evm-airdrop-svc/internal/service/api" | ||
"github.com/rarimo/evm-airdrop-svc/internal/service/api/models" | ||
"github.com/rarimo/evm-airdrop-svc/internal/service/api/requests" | ||
"github.com/rarimo/evm-airdrop-svc/resources" | ||
"gitlab.com/distributed_lab/ape" | ||
"gitlab.com/distributed_lab/ape/problems" | ||
"gitlab.com/distributed_lab/logan/v3" | ||
"gitlab.com/distributed_lab/logan/v3/errors" | ||
) | ||
|
||
var ErrInsufficienciesAmount = errors.New("amount is insufficient to pay tx fee") | ||
|
||
type TransferTxParams struct { | ||
amount *big.Int | ||
fee *big.Int | ||
gasPrice *big.Int | ||
gasLimit uint64 | ||
noSend bool | ||
} | ||
|
||
func GetTransferParams(w http.ResponseWriter, r *http.Request) { | ||
req, err := requests.NewTransferERC20Token(r) | ||
if err != nil { | ||
api.Log(r).WithError(err).Error("failed to parse request") | ||
ape.RenderErr(w, problems.BadRequest(err)...) | ||
return | ||
} | ||
|
||
txParams, err := EstimateTransfer(r, req.Data.Attributes) | ||
if err != nil { | ||
api.Log(r).WithError(err).Error("failed to estimate transfer transaction") | ||
if pkgErrors.Is(err, ErrInsufficienciesAmount) { | ||
ape.RenderErr(w, problems.BadRequest(validation.Errors{ | ||
"data/attributes/amount": errors.From(err, logan.F{ | ||
"amount": txParams.amount, | ||
"fee": txParams.fee, | ||
}), | ||
})...) | ||
return | ||
} | ||
ape.RenderErr(w, problems.InternalError()) | ||
return | ||
} | ||
|
||
ape.Render(w, models.NewEstimateResponse(txParams.amount, txParams.fee)) | ||
} | ||
|
||
func EstimateTransfer(r *http.Request, attr resources.TransferErc20TokenAttributes) (*TransferTxParams, error) { | ||
halfAmount := new(big.Int).Div(attr.Amount, big.NewInt(2)) | ||
|
||
tx, err := MakeTransferWithPermitTx(r, attr, TransferTxParams{ | ||
noSend: true, | ||
fee: halfAmount, | ||
amount: halfAmount, | ||
}) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "failed to build transfer tx") | ||
} | ||
|
||
broadcaster := api.Broadcaster(r) | ||
gasPrice := broadcaster.MultiplyGasPrice(tx.GasPrice()) | ||
feeAmount, err := buildFeeTransferAmount(r, gasPrice, tx.Gas()) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "failed to build fee transfer amount") | ||
} | ||
|
||
amount := new(big.Int).Sub(attr.Amount, feeAmount) | ||
|
||
params := TransferTxParams{ | ||
amount: amount, | ||
fee: feeAmount, | ||
gasPrice: gasPrice, | ||
gasLimit: tx.Gas(), | ||
} | ||
|
||
if amount.Cmp(new(big.Int)) != 1 { | ||
return ¶ms, ErrInsufficienciesAmount | ||
} | ||
|
||
return ¶ms, nil | ||
} | ||
|
||
func MakeTransferWithPermitTx( | ||
r *http.Request, | ||
attr resources.TransferErc20TokenAttributes, | ||
params TransferTxParams, | ||
) (*types.Transaction, error) { | ||
var ( | ||
R [32]byte | ||
S [32]byte | ||
) | ||
|
||
txOptions, err := bind.NewKeyedTransactorWithChainID(api.Broadcaster(r).PrivateKey, api.Broadcaster(r).ChainID) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "failed to get tx options") | ||
} | ||
txOptions.NoSend = params.noSend | ||
txOptions.GasPrice = params.gasPrice | ||
txOptions.GasLimit = params.gasLimit | ||
|
||
copy(R[:], hexutil.MustDecode(attr.R)) | ||
copy(S[:], hexutil.MustDecode(attr.S)) | ||
|
||
tx, err := api.ERC20PermitTransfer(r).TransferWithPermit( | ||
txOptions, | ||
api.AirdropConfig(r).TokenAddress, | ||
attr.Sender, | ||
attr.Receiver, | ||
params.amount, | ||
params.fee, | ||
attr.Deadline, | ||
attr.V, | ||
R, | ||
S, | ||
) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "failed to build transfer with permit transaction") | ||
} | ||
|
||
return tx, nil | ||
} | ||
|
||
func buildFeeTransferAmount(r *http.Request, gweiGasPrice *big.Int, gasLimit uint64) (*big.Int, error) { | ||
dollarInEth, err := api.PriceApiConfig(r).ConvertPrice() | ||
if err != nil { | ||
return nil, errors.Wrap(err, "failed to convert dollar price in eth") | ||
} | ||
|
||
// Convert GWEI gas price to ETH gas price | ||
ethGasPrice := new(big.Float).Quo(new(big.Float).SetInt(gweiGasPrice), big.NewFloat(1e18)) | ||
// Convert ETH gas price to dollar correspondence | ||
gasPriceInGlo := new(big.Float).Quo(ethGasPrice, dollarInEth) | ||
|
||
feeAmount := new(big.Float).Mul(new(big.Float).SetUint64(gasLimit), gasPriceInGlo) | ||
|
||
amount, _ := feeAmount.Int(nil) | ||
|
||
return amount, nil | ||
} |
Oops, something went wrong.