Commit a91214c9 authored by vicotor's avatar vicotor

add swap router support

parent c3edc3cd
This diff is collapsed.
......@@ -1433,5 +1433,43 @@
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "toChainID",
"type": "uint256"
},
{
"internalType": "address",
"name": "receiver",
"type": "address"
},
{
"internalType": "address",
"name": "fromToken",
"type": "address"
},
{
"internalType": "address",
"name": "toToken",
"type": "address"
}
],
"name": "outTransferSwap",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
\ No newline at end of file
package router
const UniswapV2RouterABI = `[
{
"constant": true,
"inputs": [
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
}
],
"name": "getAmountsOut",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]`
package router
import (
"context"
"errors"
"fmt"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"math/big"
"strings"
)
// Pre-parse the router ABI once at init to avoid repeated JSON parsing cost.
var (
parsedRouterABI abi.ABI
parseRouterErr error
)
func init() {
parsedRouterABI, parseRouterErr = abi.JSON(strings.NewReader(UniswapV2RouterABI))
}
// GetAmountsOut queries the router for the output amount given an input amount and swap path.
// Returns the final output amount (last element of amounts). It validates inputs and avoids panics
// on unexpected empty results.
func GetAmountsOut(
client *ethclient.Client,
user common.Address,
router common.Address,
amountIn *big.Int,
path []string,
) (*big.Int, error) {
if parseRouterErr != nil {
return nil, fmt.Errorf("router ABI init: %w", parseRouterErr)
}
if client == nil {
return nil, errors.New("nil eth client")
}
if amountIn == nil {
return nil, errors.New("nil amountIn")
}
if len(path) < 2 {
return nil, fmt.Errorf("path length must be >= 2, got %d", len(path))
}
addrPath := make([]common.Address, 0, len(path))
for _, addrStr := range path {
if !common.IsHexAddress(addrStr) || common.HexToAddress(addrStr) == (common.Address{}) {
return nil, fmt.Errorf("invalid address in path: %s", addrStr)
}
addrPath = append(addrPath, common.HexToAddress(addrStr))
}
contract := bind.NewBoundContract(router, parsedRouterABI, client, client, client)
callOpts := &bind.CallOpts{
Context: context.Background(),
From: user, // optional; included for completeness (some routers ignore).
}
var amounts = make([]interface{}, len(path))
if err := contract.Call(callOpts, &amounts, "getAmountsOut", amountIn, addrPath); err != nil {
return nil, fmt.Errorf("call getAmountsOut (amountIn=%s pathLen=%d): %w", amountIn.String(), len(path), err)
}
amountOut := *abi.ConvertType(amounts[len(path)-1], new(*big.Int)).(**big.Int)
return amountOut, nil
}
......@@ -124,3 +124,11 @@ func (d *Dao) GetOutConfig(chainId int64, token common.Address, toChainId int64)
}
return syncer.GetOutConfig(token, toChainId)
}
func (d *Dao) GetChainConfig(chainId int64) (ChainInfo, error) {
chainInfo, ok := d.chainGroup[chainId]
if !ok {
return ChainInfo{}, fmt.Errorf("chain %d config not found", chainId)
}
return chainInfo, nil
}
This diff is collapsed.
package api
type ToToken struct {
type ToBridgeToken struct {
TokenContract string `json:"token_contract" bson:"token_contract"`
ToChainId int64 `json:"to_chain_id" bson:"to_chain_id"`
ToToken string `json:"to_token" bson:"to_token"`
......@@ -8,29 +8,53 @@ type ToToken struct {
Fee string `json:"fee" bson:"fee"`
MaxLimit string `json:"max_limit" bson:"max_limit"`
}
type ChainConfig struct {
Chain string `json:"chain" bson:"chain"`
ChainId int64 `json:"chain_id" bson:"chain_id"`
BridgeContract string `json:"contract" bson:"contract"`
SupportTokens map[string]ToToken `json:"support_tokens" bson:"support_tokens"`
ExplorerUrl string `json:"explorer_url" bson:"explorer_url"`
RpcUrl string `json:"rpc" bson:"rpc"`
type SwapPath struct {
SwapFromToken string `json:"swap_from_token" bson:"swap_from_token"`
BridgeFromToken string `json:"bridge_from_token" bson:"bridge_from_token"`
BridgeToToken string `json:"bridge_to_token" bson:"bridge_to_token"`
SwapToToken string `json:"swap_to_token" bson:"swap_to_token"`
}
type ToSwapToken struct {
ToChainId int64 `json:"to_chain_id" bson:"to_chain_id"`
ToToken string `json:"to_token" bson:"to_token"`
ToTokenSymbol string `json:"to_token_symbol" bson:"to_token_symbol"`
Path SwapPath `json:"path" bson:"path"`
}
type TokenInfo struct {
Name string `json:"name" bson:"name"`
Contract string `json:"contract" bson:"contract"`
type SupportBridgeTokenInfo struct {
TokenContract string `json:"token_contract" bson:"token_contract"`
TokenSymbol string `json:"token_symbol" bson:"token_symbol"`
BridgeTokens []ToBridgeToken `json:"bridge_tokens" bson:"bridge_tokens"`
}
type SwapPair struct {
From TokenInfo `json:"from" bson:"from"`
To TokenInfo `json:"to" bson:"to"`
SwapContract string `json:"swap_contract" bson:"swap_contract"`
SwapPath []string `json:"swap_path" bson:"swap_path"`
type SupportSwapTokenInfo struct {
TokenContract string `json:"token_contract" bson:"token_contract"`
TokenSymbol string `json:"token_symbol" bson:"token_symbol"`
SwapTokens map[int64]map[string]ToSwapToken `json:"swap_tokens" bson:"swap_tokens"`
}
type ChainConfig struct {
Chain string `json:"chain" bson:"chain"`
ChainId int64 `json:"chain_id" bson:"chain_id"`
BridgeContract string `json:"contract" bson:"contract"`
SupportTokens map[string]SupportBridgeTokenInfo `json:"support_tokens" bson:"support_tokens"`
ExplorerUrl string `json:"explorer_url" bson:"explorer_url"`
RpcUrl string `json:"rpc" bson:"rpc"`
}
type ChainSwapConfig struct {
Chain string `json:"chain" bson:"chain"`
ChainId int64 `json:"chain_id" bson:"chain_id"`
BridgeContract string `json:"contract" bson:"contract"`
SupportTokens map[string]SupportSwapTokenInfo `json:"support_tokens" bson:"support_tokens"`
ExplorerUrl string `json:"explorer_url" bson:"explorer_url"`
RpcUrl string `json:"rpc" bson:"rpc"`
}
type SwapConfigs struct {
SwapPairs map[string]SwapPair `json:"swap_pairs" bson:"swap_pairs"`
Chains map[string]ChainSwapConfig `json:"chains" bson:"chains"`
}
type BridgeConfig struct {
......@@ -58,8 +82,54 @@ type History struct {
Finish []*HistoryInfo `json:"finish" bson:"finish"`
}
type RouterQuery struct {
ChainId int64 `json:"chain_id"`
User string `json:"user"`
}
type QuoteBridgeParam struct {
FromChainId int64 `json:"from_chain_id"`
ToChainId int64 `json:"to_chain_id"`
FromToken string `json:"from_token"`
ToToken string `json:"to_token"`
InputAmount string `json:"amount"`
User string `json:"user"`
Receiver string `json:"receiver"`
}
type QuoteSwapParam struct {
FromChainId int64 `json:"from_chain_id"`
ToChainId int64 `json:"to_chain_id"`
Path SwapPath `json:"path"`
InputAmount string `json:"amount"`
User string `json:"user"`
Receiver string `json:"receiver"`
}
type TokenBalance struct {
Name string `json:"name"`
Decimals int `json:"decimals"`
Balance string `json:"balance"`
Contract string `json:"contract"`
}
type TokenBalances struct {
Balances []TokenBalance `json:"balances"`
}
type QuoteResult struct {
ToContract string `json:"to_contract"`
OutAmount string `json:"out_amount"`
Payload string `json:"payload"`
}
type Querier interface {
GetBridgeConfig() (config BridgeConfig, err error)
GetSwapConfig(chainId int64) (config SwapConfigs, err error)
GetAllChainSwapConfig() (map[int64]*ChainSwapConfig, error)
GetSwapConfig(int64) (*ChainSwapConfig, error)
GetHistoryInfo(user string) (history History, err error)
GetBridgeTokenBalance(chainId int64, user string) (balances TokenBalances, err error)
QuoteBridge(param QuoteBridgeParam) (quote QuoteResult, err error)
GetSwapTokenBalance(chainId int64, user string) (balances TokenBalances, err error)
QuoteSwap(param QuoteSwapParam) (quote QuoteResult, err error)
}
......@@ -2,6 +2,7 @@ package server
import (
"code.wuban.net.cn/movabridge/bridge-backend/constant"
apiModel "code.wuban.net.cn/movabridge/bridge-backend/model/api"
"github.com/ethereum/go-ethereum/common"
log "github.com/sirupsen/logrus"
"strconv"
......@@ -47,21 +48,113 @@ func getHistory(c *gin.Context) {
c.JSON(200, withSuccess(history))
}
func getSwapConfig(c *gin.Context) {
chainIdStr := c.DefaultQuery("chain", "")
chainId, err := strconv.ParseInt(chainIdStr, 10, 64)
func bridgeRouters(c *gin.Context) {
if _querier == nil {
log.Error("querier is nil")
c.JSON(500, withError(constant.InternalError))
return
}
var queryParam apiModel.RouterQuery
if err := c.ShouldBindQuery(&queryParam); err != nil {
log.Errorf("bind query param error: %v", err)
c.JSON(200, withError(constant.InvalidParam))
return
}
config, err := _querier.GetBridgeConfig()
if err != nil {
log.Errorf("convert chainId(%s) to int error: %v", chainIdStr, err)
log.Errorf("get bridge config error: %v", err)
c.JSON(500, withError(constant.InternalError))
return
}
c.JSON(200, withSuccess(config))
}
func bridgeBalance(c *gin.Context) {
if _querier == nil {
log.Error("querier is nil")
c.JSON(500, withError(constant.InternalError))
return
}
var queryParam apiModel.RouterQuery
if err := c.ShouldBindQuery(&queryParam); err != nil {
log.Errorf("bind query param error: %v", err)
c.JSON(200, withError(constant.InvalidParam))
return
}
if !common.IsHexAddress(queryParam.User) {
log.Errorf("invalid user address: %v", queryParam.User)
c.JSON(200, withError(constant.InvalidParam))
}
// get all tokens
balances, err := _querier.GetBridgeTokenBalance(queryParam.ChainId, queryParam.User)
if err != nil {
log.Errorf("get bridge balance error: %v", err)
c.JSON(500, withError(constant.InternalError))
return
}
c.JSON(200, withSuccess(balances))
}
func bridgeQuote(c *gin.Context) {
if _querier == nil {
log.Error("querier is nil")
c.JSON(500, withError(constant.InternalError))
return
}
var queryParam apiModel.QuoteBridgeParam
if err := c.ShouldBindQuery(&queryParam); err != nil {
log.Errorf("bind query param error: %v", err)
c.JSON(200, withError(constant.InvalidParam))
return
}
if !common.IsHexAddress(queryParam.User) {
log.Errorf("invalid user address: %v", queryParam.User)
c.JSON(200, withError(constant.InvalidParam))
return
}
if !common.IsHexAddress(queryParam.Receiver) {
log.Errorf("invalid receiver address: %v", queryParam.Receiver)
c.JSON(200, withError(constant.InvalidParam))
return
}
quote, err := _querier.QuoteBridge(queryParam)
if err != nil {
log.Errorf("get bridge quote error: %v", err)
c.JSON(500, withError(constant.InternalError))
return
}
c.JSON(200, withSuccess(quote))
}
func getSwapRouters(c *gin.Context) {
if _querier == nil {
log.Error("querier is nil")
c.JSON(500, withError(constant.InternalError))
return
}
var queryParam apiModel.RouterQuery
if err := c.ShouldBindQuery(&queryParam); err != nil {
log.Errorf("bind query param error: %v", err)
c.JSON(200, withError(constant.InvalidParam))
return
}
configs, err := _querier.GetSwapConfig(queryParam.ChainId)
if err != nil {
log.Errorf("get swap config error: %v", err)
c.JSON(500, withError(constant.InternalError))
return
}
c.JSON(200, withSuccess(configs))
}
configs, err := _querier.GetSwapConfig(chainId)
func getAllSwapRouters(c *gin.Context) {
if _querier == nil {
log.Error("querier is nil")
c.JSON(500, withError(constant.InternalError))
return
}
configs, err := _querier.GetAllChainSwapConfig()
if err != nil {
log.Errorf("get swap config error: %v", err)
c.JSON(500, withError(constant.InternalError))
......@@ -69,3 +162,86 @@ func getSwapConfig(c *gin.Context) {
}
c.JSON(200, withSuccess(configs))
}
func swapBalance(c *gin.Context) {
if _querier == nil {
log.Error("querier is nil")
c.JSON(500, withError(constant.InternalError))
return
}
var queryParam apiModel.RouterQuery
if err := c.ShouldBindQuery(&queryParam); err != nil {
log.Errorf("bind query param error: %v", err)
c.JSON(200, withError(constant.InvalidParam))
return
}
if !common.IsHexAddress(queryParam.User) {
log.Errorf("invalid user address: %v", queryParam.User)
c.JSON(200, withError(constant.InvalidParam))
}
// get all tokens
balances, err := _querier.GetSwapTokenBalance(queryParam.ChainId, queryParam.User)
if err != nil {
log.Errorf("get swap balance error: %v", err)
c.JSON(500, withError(constant.InternalError))
return
}
c.JSON(200, withSuccess(balances))
}
func swapQuote(c *gin.Context) {
if _querier == nil {
log.Error("querier is nil")
c.JSON(500, withError(constant.InternalError))
return
}
var queryParam apiModel.QuoteSwapParam
if err := c.ShouldBindQuery(&queryParam); err != nil {
log.Errorf("bind query param error: %v", err)
c.JSON(200, withError(constant.InvalidParam))
return
}
if !common.IsHexAddress(queryParam.User) {
log.Errorf("invalid user address: %v", queryParam.User)
c.JSON(200, withError(constant.InvalidParam))
return
}
if !common.IsHexAddress(queryParam.Receiver) {
log.Errorf("invalid receiver address: %v", queryParam.Receiver)
c.JSON(200, withError(constant.InvalidParam))
return
}
quote, err := _querier.QuoteSwap(queryParam)
if err != nil {
log.Errorf("get swap quote error: %v", err)
c.JSON(500, withError(constant.InternalError))
return
}
c.JSON(200, withSuccess(quote))
}
func getSwapRoutersByChainId(c *gin.Context) {
if _querier == nil {
log.Error("querier is nil")
c.JSON(500, withError(constant.InternalError))
return
}
chainIdStr := c.Param("chainid")
if chainIdStr == "" {
c.JSON(200, withError(constant.InvalidParam))
return
}
chainId, err := strconv.ParseInt(chainIdStr, 10, 64)
if err != nil {
log.Errorf("convert chainid(%s) error: %v", chainIdStr, err)
c.JSON(200, withError(constant.InvalidParam))
return
}
config, err := _querier.GetSwapConfig(chainId)
if err != nil {
log.Errorf("get swap config for chain %d error: %v", chainId, err)
c.JSON(500, withError(constant.InternalError))
return
}
c.JSON(200, withSuccess(config))
}
......@@ -11,10 +11,24 @@ func initRouter(conf *config.Config, e *gin.Engine) {
e.Use(middleware.CheckHeaderMiddleware(conf.Server.InvalidHeaders))
v1 := e.Group("/api/v1")
v1.GET("/params", getParam)
{
user := v1.Group("/user")
user.GET("/history", getHistory)
}
v1.GET("/params", getParam)
v1.GET("/swapconfig", getSwapConfig)
{
bridge := v1.Group("/bridge")
bridge.POST("/routers", bridgeRouters)
bridge.POST("/balance", bridgeBalance)
bridge.POST("/quote", bridgeQuote)
}
{
swap := v1.Group("/swap")
swap.GET("/routers", getSwapRouters)
swap.GET("/routers/:chain", getSwapRoutersByChainId)
swap.POST("/balance", swapBalance)
swap.POST("/quote", swapQuote)
}
}
......@@ -9,6 +9,7 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"math/big"
"strings"
"sync"
)
......@@ -88,3 +89,51 @@ func (tr *TokenRepo) RetriveTokenInfo(chainId int64, address string) (TokenInfo,
tr.SetTokenInfo(address, info)
return info, nil
}
func (tr *TokenRepo) RetriveTokenInfoAndBalance(client *ethclient.Client, address string, user string) (TokenInfo, *big.Int, error) {
info := TokenInfo{}
balance := big.NewInt(0)
contract, err := token.NewTokenCaller(common.HexToAddress(address), client)
if err != nil {
return info, balance, fmt.Errorf("fail to connect contract err: %v", err)
}
callOpt := &bind.CallOpts{
BlockNumber: nil,
From: common.HexToAddress(user),
Context: context.Background(),
}
userBalance, err := contract.BalanceOf(callOpt, common.HexToAddress(user))
if err != nil {
return info, balance, err
} else {
balance = userBalance
}
if value, ok := tr.GetTokenInfo(address); ok {
info = value
} else {
name, err := contract.Name(callOpt)
if err != nil {
return info, balance, err
}
symbol, err := contract.Symbol(callOpt)
if err != nil {
return info, balance, err
}
decimals, err := contract.Decimals(callOpt)
if err != nil {
return info, balance, err
}
info := TokenInfo{
Name: name,
Symbol: symbol,
Decimals: decimals.Int64(),
Address: address,
}
tr.SetTokenInfo(address, info)
}
return info, balance, nil
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment