Skip to content

Commit

Permalink
add: requests to estimate and transfer ERC20; update: configs with pr…
Browse files Browse the repository at this point in the history
…ice api and contracts;
  • Loading branch information
mhrynenko committed May 31, 2024
1 parent 59f5e40 commit e9158f4
Show file tree
Hide file tree
Showing 10 changed files with 576 additions and 21 deletions.
10 changes: 9 additions & 1 deletion config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ broadcaster:
chain_id: chain_id
sender_private_key: priv_key
query_limit: 10
erc20_permit_transfer: contract_address
gas_multiplier: 1.1

verifier:
verification_key_path: "./verification_key.json"
allowed_age: 18
allowed_citizenships: ["UKR"]
allowed_citizenships: [ "UKR" ]
allowed_event_id: "event_id"
allowed_query_selector: "query_selector"
# at least one of these should be correct to pass:
Expand All @@ -32,3 +34,9 @@ root_verifier:
rpc: evm_rpc_url
contract: registration_contract_address
request_timeout: 10s

price_api:
url: api_url #coinmarketcap
key: api_key
currency_id: 23888
quote_tag: "ETH"
2 changes: 1 addition & 1 deletion internal/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import (
"syscall"

"github.com/alecthomas/kingpin"
"github.com/rarimo/evm-airdrop-svc/internal/broadcaster"
"github.com/rarimo/evm-airdrop-svc/internal/config"
"github.com/rarimo/evm-airdrop-svc/internal/service"
"github.com/rarimo/evm-airdrop-svc/internal/service/broadcaster"
"gitlab.com/distributed_lab/kit/kv"
"gitlab.com/distributed_lab/logan/v3"
)
Expand Down
2 changes: 2 additions & 0 deletions internal/config/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Config struct {
identity.VerifierProvider
Broadcasterer
AirdropConfiger
PriceApiConfiger

airdrop comfig.Once
verifier comfig.Once
Expand All @@ -29,5 +30,6 @@ func New(getter kv.Getter) *Config {
VerifierProvider: identity.NewVerifierProvider(getter),
Broadcasterer: NewBroadcaster(getter),
AirdropConfiger: NewAirdropConfiger(getter),
PriceApiConfiger: NewPriceApiConfiger(getter),
}
}
129 changes: 129 additions & 0 deletions internal/config/price.go
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
}
43 changes: 43 additions & 0 deletions internal/service/api/handlers/get_airdrop.go
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))
}
150 changes: 150 additions & 0 deletions internal/service/api/handlers/get_transfer_params.go
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 &params, ErrInsufficienciesAmount
}

return &params, 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
}
Loading

0 comments on commit e9158f4

Please sign in to comment.