Commit a91214c9 authored by vicotor's avatar vicotor

add swap router support

parent c3edc3cd
This diff is collapsed.
...@@ -1433,5 +1433,43 @@ ...@@ -1433,5 +1433,43 @@
], ],
"stateMutability": "view", "stateMutability": "view",
"type": "function" "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) ...@@ -124,3 +124,11 @@ func (d *Dao) GetOutConfig(chainId int64, token common.Address, toChainId int64)
} }
return syncer.GetOutConfig(token, toChainId) 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 package api
type ToToken struct { type ToBridgeToken struct {
TokenContract string `json:"token_contract" bson:"token_contract"` TokenContract string `json:"token_contract" bson:"token_contract"`
ToChainId int64 `json:"to_chain_id" bson:"to_chain_id"` ToChainId int64 `json:"to_chain_id" bson:"to_chain_id"`
ToToken string `json:"to_token" bson:"to_token"` ToToken string `json:"to_token" bson:"to_token"`
...@@ -8,29 +8,53 @@ type ToToken struct { ...@@ -8,29 +8,53 @@ type ToToken struct {
Fee string `json:"fee" bson:"fee"` Fee string `json:"fee" bson:"fee"`
MaxLimit string `json:"max_limit" bson:"max_limit"` MaxLimit string `json:"max_limit" bson:"max_limit"`
} }
type ChainConfig struct {
Chain string `json:"chain" bson:"chain"` type SwapPath struct {
ChainId int64 `json:"chain_id" bson:"chain_id"` SwapFromToken string `json:"swap_from_token" bson:"swap_from_token"`
BridgeContract string `json:"contract" bson:"contract"` BridgeFromToken string `json:"bridge_from_token" bson:"bridge_from_token"`
SupportTokens map[string]ToToken `json:"support_tokens" bson:"support_tokens"` BridgeToToken string `json:"bridge_to_token" bson:"bridge_to_token"`
ExplorerUrl string `json:"explorer_url" bson:"explorer_url"` SwapToToken string `json:"swap_to_token" bson:"swap_to_token"`
RpcUrl string `json:"rpc" bson:"rpc"` }
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 { type SupportBridgeTokenInfo struct {
Name string `json:"name" bson:"name"` TokenContract string `json:"token_contract" bson:"token_contract"`
Contract string `json:"contract" bson:"contract"` TokenSymbol string `json:"token_symbol" bson:"token_symbol"`
BridgeTokens []ToBridgeToken `json:"bridge_tokens" bson:"bridge_tokens"`
} }
type SwapPair struct { type SupportSwapTokenInfo struct {
From TokenInfo `json:"from" bson:"from"` TokenContract string `json:"token_contract" bson:"token_contract"`
To TokenInfo `json:"to" bson:"to"` TokenSymbol string `json:"token_symbol" bson:"token_symbol"`
SwapContract string `json:"swap_contract" bson:"swap_contract"` SwapTokens map[int64]map[string]ToSwapToken `json:"swap_tokens" bson:"swap_tokens"`
SwapPath []string `json:"swap_path" bson:"swap_path"` }
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 { type SwapConfigs struct {
SwapPairs map[string]SwapPair `json:"swap_pairs" bson:"swap_pairs"` Chains map[string]ChainSwapConfig `json:"chains" bson:"chains"`
} }
type BridgeConfig struct { type BridgeConfig struct {
...@@ -58,8 +82,54 @@ type History struct { ...@@ -58,8 +82,54 @@ type History struct {
Finish []*HistoryInfo `json:"finish" bson:"finish"` 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 { type Querier interface {
GetBridgeConfig() (config BridgeConfig, err error) 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) 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 ...@@ -2,6 +2,7 @@ package server
import ( import (
"code.wuban.net.cn/movabridge/bridge-backend/constant" "code.wuban.net.cn/movabridge/bridge-backend/constant"
apiModel "code.wuban.net.cn/movabridge/bridge-backend/model/api"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"strconv" "strconv"
...@@ -47,21 +48,113 @@ func getHistory(c *gin.Context) { ...@@ -47,21 +48,113 @@ func getHistory(c *gin.Context) {
c.JSON(200, withSuccess(history)) c.JSON(200, withSuccess(history))
} }
func getSwapConfig(c *gin.Context) { func bridgeRouters(c *gin.Context) {
chainIdStr := c.DefaultQuery("chain", "") if _querier == nil {
chainId, err := strconv.ParseInt(chainIdStr, 10, 64) 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 { 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)) 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 { if _querier == nil {
log.Error("querier is nil") log.Error("querier is nil")
c.JSON(500, withError(constant.InternalError)) c.JSON(500, withError(constant.InternalError))
return 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 { if err != nil {
log.Errorf("get swap config error: %v", err) log.Errorf("get swap config error: %v", err)
c.JSON(500, withError(constant.InternalError)) c.JSON(500, withError(constant.InternalError))
...@@ -69,3 +162,86 @@ func getSwapConfig(c *gin.Context) { ...@@ -69,3 +162,86 @@ func getSwapConfig(c *gin.Context) {
} }
c.JSON(200, withSuccess(configs)) 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) { ...@@ -11,10 +11,24 @@ func initRouter(conf *config.Config, e *gin.Engine) {
e.Use(middleware.CheckHeaderMiddleware(conf.Server.InvalidHeaders)) e.Use(middleware.CheckHeaderMiddleware(conf.Server.InvalidHeaders))
v1 := e.Group("/api/v1") v1 := e.Group("/api/v1")
v1.GET("/params", getParam)
{ {
user := v1.Group("/user") user := v1.Group("/user")
user.GET("/history", getHistory) 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 ( ...@@ -9,6 +9,7 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
"math/big"
"strings" "strings"
"sync" "sync"
) )
...@@ -88,3 +89,51 @@ func (tr *TokenRepo) RetriveTokenInfo(chainId int64, address string) (TokenInfo, ...@@ -88,3 +89,51 @@ func (tr *TokenRepo) RetriveTokenInfo(chainId int64, address string) (TokenInfo,
tr.SetTokenInfo(address, info) tr.SetTokenInfo(address, info)
return info, nil 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