feat: swap via paraswap

This commit is contained in:
Sale Djenic 2024-04-01 15:39:17 +02:00 committed by saledjenic
parent c921079761
commit 7b09ee073d
20 changed files with 799 additions and 52 deletions

View File

@ -18,7 +18,7 @@ var contractAddressByChainID = map[uint64]common.Address{
func ContractAddress(chainID uint64) (common.Address, error) { func ContractAddress(chainID uint64) (common.Address, error) {
addr, exists := contractAddressByChainID[chainID] addr, exists := contractAddressByChainID[chainID]
if !exists { if !exists {
return *new(common.Address), ErrorNotAvailableOnChainID return common.Address{}, ErrorNotAvailableOnChainID
} }
return addr, nil return addr, nil
} }

View File

@ -426,6 +426,7 @@ func (api *API) GetSuggestedRoutes(
addrTo common.Address, addrTo common.Address,
amountIn *hexutil.Big, amountIn *hexutil.Big,
tokenID string, tokenID string,
toTokenID string,
disabledFromChainIDs, disabledFromChainIDs,
disabledToChaindIDs, disabledToChaindIDs,
preferedChainIDs []uint64, preferedChainIDs []uint64,
@ -433,7 +434,7 @@ func (api *API) GetSuggestedRoutes(
fromLockedAmount map[uint64]*hexutil.Big, fromLockedAmount map[uint64]*hexutil.Big,
) (*SuggestedRoutes, error) { ) (*SuggestedRoutes, error) {
log.Debug("call to GetSuggestedRoutes") log.Debug("call to GetSuggestedRoutes")
return api.router.suggestedRoutes(ctx, sendType, addrFrom, addrTo, amountIn.ToInt(), tokenID, disabledFromChainIDs, return api.router.suggestedRoutes(ctx, sendType, addrFrom, addrTo, amountIn.ToInt(), tokenID, toTokenID, disabledFromChainIDs,
disabledToChaindIDs, preferedChainIDs, gasFeeMode, fromLockedAmount) disabledToChaindIDs, preferedChainIDs, gasFeeMode, fromLockedAmount)
} }

View File

@ -14,6 +14,8 @@ import (
"github.com/status-im/status-go/transactions" "github.com/status-im/status-go/transactions"
) )
var ZeroAddress = common.Address{}
const IncreaseEstimatedGasFactor = 1.1 const IncreaseEstimatedGasFactor = 1.1
func getSigner(chainID uint64, from types.Address, verifiedAccount *account.SelectedExtKey) bind.SignerFn { func getSigner(chainID uint64, from types.Address, verifiedAccount *account.SelectedExtKey) bind.SignerFn {
@ -31,6 +33,7 @@ type TransactionBridge struct {
CbridgeTx *CBridgeTxArgs CbridgeTx *CBridgeTxArgs
ERC721TransferTx *ERC721TransferTxArgs ERC721TransferTx *ERC721TransferTxArgs
ERC1155TransferTx *ERC1155TransferTxArgs ERC1155TransferTx *ERC1155TransferTxArgs
SwapTx *SwapTxArgs
} }
func (t *TransactionBridge) Value() *big.Int { func (t *TransactionBridge) Value() *big.Int {
@ -99,9 +102,9 @@ func (t *TransactionBridge) Data() types.HexBytes {
type Bridge interface { type Bridge interface {
Name() string Name() string
Can(from *params.Network, to *params.Network, token *token.Token, balance *big.Int) (bool, error) Can(from *params.Network, to *params.Network, token *token.Token, toToken *token.Token, balance *big.Int) (bool, error)
CalculateFees(from, to *params.Network, token *token.Token, amountIn *big.Int, nativeTokenPrice, tokenPrice float64, gasPrice *big.Float) (*big.Int, *big.Int, error) CalculateFees(from, to *params.Network, token *token.Token, amountIn *big.Int, nativeTokenPrice, tokenPrice float64, gasPrice *big.Float) (*big.Int, *big.Int, error)
EstimateGas(fromNetwork *params.Network, toNetwork *params.Network, from common.Address, to common.Address, token *token.Token, amountIn *big.Int) (uint64, error) EstimateGas(fromNetwork *params.Network, toNetwork *params.Network, from common.Address, to common.Address, token *token.Token, toToken *token.Token, amountIn *big.Int) (uint64, error)
CalculateAmountOut(from, to *params.Network, amountIn *big.Int, symbol string) (*big.Int, error) CalculateAmountOut(from, to *params.Network, amountIn *big.Int, symbol string) (*big.Int, error)
Send(sendArgs *TransactionBridge, verifiedAccount *account.SelectedExtKey) (types.Hash, error) Send(sendArgs *TransactionBridge, verifiedAccount *account.SelectedExtKey) (types.Hash, error)
GetContractAddress(network *params.Network, token *token.Token) *common.Address GetContractAddress(network *params.Network, token *token.Token) *common.Address

View File

@ -153,7 +153,7 @@ func (s *CBridge) getTransferConfig(isTest bool) (*cbridge.GetTransferConfigsRes
return &res, nil return &res, nil
} }
func (s *CBridge) Can(from, to *params.Network, token *token.Token, balance *big.Int) (bool, error) { func (s *CBridge) Can(from, to *params.Network, token *token.Token, toToken *token.Token, balance *big.Int) (bool, error) {
if from.ChainID == to.ChainID { if from.ChainID == to.ChainID {
return false, nil return false, nil
} }
@ -221,7 +221,7 @@ func (s *CBridge) CalculateFees(from, to *params.Network, token *token.Token, am
return big.NewInt(0), new(big.Int).Add(baseFee, percFee), nil return big.NewInt(0), new(big.Int).Add(baseFee, percFee), nil
} }
func (s *CBridge) EstimateGas(fromNetwork *params.Network, toNetwork *params.Network, from common.Address, to common.Address, token *token.Token, amountIn *big.Int) (uint64, error) { func (s *CBridge) EstimateGas(fromNetwork *params.Network, toNetwork *params.Network, from common.Address, to common.Address, token *token.Token, toToken *token.Token, amountIn *big.Int) (uint64, error) {
var input []byte var input []byte
value := new(big.Int) value := new(big.Int)

View File

@ -41,7 +41,7 @@ func (s *ERC1155TransferBridge) Name() string {
return "ERC1155Transfer" return "ERC1155Transfer"
} }
func (s *ERC1155TransferBridge) Can(from, to *params.Network, token *token.Token, balance *big.Int) (bool, error) { func (s *ERC1155TransferBridge) Can(from, to *params.Network, token *token.Token, toToken *token.Token, balance *big.Int) (bool, error) {
return from.ChainID == to.ChainID, nil return from.ChainID == to.ChainID, nil
} }
@ -49,7 +49,7 @@ func (s *ERC1155TransferBridge) CalculateFees(from, to *params.Network, token *t
return big.NewInt(0), big.NewInt(0), nil return big.NewInt(0), big.NewInt(0), nil
} }
func (s *ERC1155TransferBridge) EstimateGas(fromNetwork *params.Network, toNetwork *params.Network, from common.Address, to common.Address, token *token.Token, amountIn *big.Int) (uint64, error) { func (s *ERC1155TransferBridge) EstimateGas(fromNetwork *params.Network, toNetwork *params.Network, from common.Address, to common.Address, token *token.Token, toToken *token.Token, amountIn *big.Int) (uint64, error) {
ethClient, err := s.rpcClient.EthClient(fromNetwork.ChainID) ethClient, err := s.rpcClient.EthClient(fromNetwork.ChainID)
if err != nil { if err != nil {
return 0, err return 0, err

View File

@ -40,7 +40,7 @@ func (s *ERC721TransferBridge) Name() string {
return "ERC721Transfer" return "ERC721Transfer"
} }
func (s *ERC721TransferBridge) Can(from, to *params.Network, token *token.Token, balance *big.Int) (bool, error) { func (s *ERC721TransferBridge) Can(from, to *params.Network, token *token.Token, toToken *token.Token, balance *big.Int) (bool, error) {
return from.ChainID == to.ChainID, nil return from.ChainID == to.ChainID, nil
} }
@ -48,7 +48,7 @@ func (s *ERC721TransferBridge) CalculateFees(from, to *params.Network, token *to
return big.NewInt(0), big.NewInt(0), nil return big.NewInt(0), big.NewInt(0), nil
} }
func (s *ERC721TransferBridge) EstimateGas(fromNetwork *params.Network, toNetwork *params.Network, from common.Address, to common.Address, token *token.Token, amountIn *big.Int) (uint64, error) { func (s *ERC721TransferBridge) EstimateGas(fromNetwork *params.Network, toNetwork *params.Network, from common.Address, to common.Address, token *token.Token, toToken *token.Token, amountIn *big.Int) (uint64, error) {
ethClient, err := s.rpcClient.EthClient(fromNetwork.ChainID) ethClient, err := s.rpcClient.EthClient(fromNetwork.ChainID)
if err != nil { if err != nil {
return 0, err return 0, err

View File

@ -108,7 +108,7 @@ func (h *HopBridge) Name() string {
return "Hop" return "Hop"
} }
func (h *HopBridge) Can(from, to *params.Network, token *token.Token, balance *big.Int) (bool, error) { func (h *HopBridge) Can(from, to *params.Network, token *token.Token, toToken *token.Token, balance *big.Int) (bool, error) {
if balance.Cmp(big.NewInt(0)) == 0 { if balance.Cmp(big.NewInt(0)) == 0 {
return false, nil return false, nil
} }
@ -132,7 +132,7 @@ func (h *HopBridge) Can(from, to *params.Network, token *token.Token, balance *b
return true, nil return true, nil
} }
func (h *HopBridge) EstimateGas(fromNetwork *params.Network, toNetwork *params.Network, from common.Address, to common.Address, token *token.Token, amountIn *big.Int) (uint64, error) { func (h *HopBridge) EstimateGas(fromNetwork *params.Network, toNetwork *params.Network, from common.Address, to common.Address, token *token.Token, toToken *token.Token, amountIn *big.Int) (uint64, error) {
var input []byte var input []byte
value := new(big.Int) value := new(big.Int)

View File

@ -0,0 +1,202 @@
package bridge
import (
"context"
"errors"
"math/big"
"strconv"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
ethTypes "github.com/ethereum/go-ethereum/core/types"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/rpc"
walletCommon "github.com/status-im/status-go/services/wallet/common"
"github.com/status-im/status-go/services/wallet/thirdparty/paraswap"
"github.com/status-im/status-go/services/wallet/token"
walletToken "github.com/status-im/status-go/services/wallet/token"
"github.com/status-im/status-go/transactions"
)
type SwapTxArgs struct {
transactions.SendTxArgs
ChainID uint64 `json:"chainId"`
}
type SwapParaswap struct {
paraswapClient *paraswap.ClientV5
priceRoute paraswap.Route
transactor *transactions.Transactor
}
func NewSwapParaswap(rpcClient *rpc.Client, transactor *transactions.Transactor, tokenManager *walletToken.Manager) *SwapParaswap {
return &SwapParaswap{
paraswapClient: paraswap.NewClientV5(walletCommon.EthereumMainnet),
transactor: transactor,
}
}
func (s *SwapParaswap) Name() string {
return "Paraswap"
}
func (s *SwapParaswap) Can(from, to *params.Network, token *walletToken.Token, toToken *walletToken.Token, balance *big.Int) (bool, error) {
if token == nil || toToken == nil {
return false, errors.New("token and toToken cannot be nil")
}
if from.ChainID != to.ChainID {
return false, nil
}
s.paraswapClient.SetChainID(from.ChainID)
searchForToken := token.Address == ZeroAddress
searchForToToken := toToken.Address == ZeroAddress
if searchForToToken || searchForToken {
tokensList, err := s.paraswapClient.FetchTokensList(context.Background())
if err != nil {
return false, err
}
for _, t := range tokensList {
if searchForToken && t.Symbol == token.Symbol {
token.Address = common.HexToAddress(t.Address)
token.Decimals = t.Decimals
if !searchForToToken {
break
}
}
if searchForToToken && t.Symbol == toToken.Symbol {
toToken.Address = common.HexToAddress(t.Address)
toToken.Decimals = t.Decimals
if !searchForToken {
break
}
}
}
}
if token.Address == ZeroAddress || toToken.Address == ZeroAddress {
return false, errors.New("cannot resolve token/s")
}
return true, nil
}
func (s *SwapParaswap) CalculateFees(from, to *params.Network, token *token.Token, amountIn *big.Int, nativeTokenPrice, tokenPrice float64, gasPrice *big.Float) (*big.Int, *big.Int, error) {
return big.NewInt(0), big.NewInt(0), nil
}
func (s *SwapParaswap) EstimateGas(fromNetwork *params.Network, toNetwork *params.Network, from common.Address, to common.Address, token *token.Token, toToken *token.Token, amountIn *big.Int) (uint64, error) {
priceRoute, err := s.paraswapClient.FetchPriceRoute(context.Background(), token.Address, token.Decimals, toToken.Address, toToken.Decimals, amountIn, from, to)
if err != nil {
return 0, err
}
s.priceRoute = priceRoute
return priceRoute.GasCost.Uint64(), nil
}
func (s *SwapParaswap) GetContractAddress(network *params.Network, token *token.Token) *common.Address {
var address common.Address
if network.ChainID == walletCommon.EthereumMainnet {
address = common.HexToAddress("0x216b4b4ba9f3e719726886d34a177484278bfcae")
} else if network.ChainID == walletCommon.ArbitrumMainnet {
address = common.HexToAddress("0x216b4b4ba9f3e719726886d34a177484278bfcae")
} else if network.ChainID == walletCommon.OptimismMainnet {
address = common.HexToAddress("0x216b4b4ba9f3e719726886d34a177484278bfcae")
}
return &address
}
func (s *SwapParaswap) BuildTx(network *params.Network, fromAddress common.Address, toAddress common.Address, token *token.Token, amountIn *big.Int) (*ethTypes.Transaction, error) {
toAddr := types.Address(toAddress)
sendArgs := &TransactionBridge{
SwapTx: &SwapTxArgs{
SendTxArgs: transactions.SendTxArgs{
From: types.Address(fromAddress),
To: &toAddr,
Value: (*hexutil.Big)(amountIn),
Data: types.HexBytes("0x0"),
Symbol: token.Symbol,
},
ChainID: network.ChainID,
},
}
return s.BuildTransaction(sendArgs)
}
func (s *SwapParaswap) prepareTransaction(sendArgs *TransactionBridge) error {
tx, err := s.paraswapClient.BuildTransaction(context.Background(), s.priceRoute.SrcTokenAddress, s.priceRoute.SrcTokenDecimals, s.priceRoute.SrcAmount.Int,
s.priceRoute.DestTokenAddress, s.priceRoute.DestTokenDecimals, s.priceRoute.DestAmount.Int, common.Address(sendArgs.SwapTx.From), common.Address(*sendArgs.SwapTx.To), s.priceRoute.RawPriceRoute)
if err != nil {
return err
}
value, ok := new(big.Int).SetString(tx.Value, 10)
if !ok {
return errors.New("error converting amount to big.Int")
}
gas, err := strconv.ParseUint(tx.Gas, 10, 64)
if err != nil {
return err
}
gasPrice, ok := new(big.Int).SetString(tx.GasPrice, 10)
if !ok {
return errors.New("error converting amount to big.Int")
}
sendArgs.ChainID = tx.ChainID
sendArgs.SwapTx.ChainID = tx.ChainID
toAddr := types.HexToAddress(tx.To)
sendArgs.SwapTx.From = types.HexToAddress(tx.From)
sendArgs.SwapTx.To = &toAddr
sendArgs.SwapTx.Value = (*hexutil.Big)(value)
sendArgs.SwapTx.Gas = (*hexutil.Uint64)(&gas)
sendArgs.SwapTx.GasPrice = (*hexutil.Big)(gasPrice)
sendArgs.SwapTx.Data = types.Hex2Bytes(tx.Data)
return nil
}
func (s *SwapParaswap) BuildTransaction(sendArgs *TransactionBridge) (*ethTypes.Transaction, error) {
err := s.prepareTransaction(sendArgs)
if err != nil {
return nil, err
}
return s.transactor.ValidateAndBuildTransaction(sendArgs.ChainID, sendArgs.SwapTx.SendTxArgs)
}
func (s *SwapParaswap) Send(sendArgs *TransactionBridge, verifiedAccount *account.SelectedExtKey) (types.Hash, error) {
txBridgeArgs := &TransactionBridge{
SwapTx: &SwapTxArgs{
SendTxArgs: transactions.SendTxArgs{
From: sendArgs.SwapTx.From,
To: sendArgs.SwapTx.To,
MultiTransactionID: sendArgs.SwapTx.MultiTransactionID,
Symbol: sendArgs.SwapTx.Symbol,
},
},
}
err := s.prepareTransaction(txBridgeArgs)
if err != nil {
return types.Hash{}, err
}
return s.transactor.SendTransactionWithChainID(txBridgeArgs.ChainID, txBridgeArgs.SwapTx.SendTxArgs, verifiedAccount)
}
func (s *SwapParaswap) CalculateAmountOut(from, to *params.Network, amountIn *big.Int, symbol string) (*big.Int, error) {
return s.priceRoute.DestAmount.Int, nil
}

View File

@ -32,7 +32,7 @@ func (s *TransferBridge) Name() string {
return "Transfer" return "Transfer"
} }
func (s *TransferBridge) Can(from, to *params.Network, token *token.Token, balance *big.Int) (bool, error) { func (s *TransferBridge) Can(from, to *params.Network, token *token.Token, toToken *token.Token, balance *big.Int) (bool, error) {
return from.ChainID == to.ChainID, nil return from.ChainID == to.ChainID, nil
} }
@ -40,7 +40,7 @@ func (s *TransferBridge) CalculateFees(from, to *params.Network, token *token.To
return big.NewInt(0), big.NewInt(0), nil return big.NewInt(0), big.NewInt(0), nil
} }
func (s *TransferBridge) EstimateGas(fromNetwork *params.Network, toNetwork *params.Network, from common.Address, to common.Address, token *token.Token, amountIn *big.Int) (uint64, error) { func (s *TransferBridge) EstimateGas(fromNetwork *params.Network, toNetwork *params.Network, from common.Address, to common.Address, token *token.Token, toToken *token.Token, amountIn *big.Int) (uint64, error) {
estimation := uint64(0) estimation := uint64(0)
var err error var err error
if token.Symbol == "ETH" { if token.Symbol == "ETH" {

View File

@ -26,6 +26,7 @@ import (
"github.com/status-im/status-go/services/wallet/bridge" "github.com/status-im/status-go/services/wallet/bridge"
walletCommon "github.com/status-im/status-go/services/wallet/common" walletCommon "github.com/status-im/status-go/services/wallet/common"
"github.com/status-im/status-go/services/wallet/token" "github.com/status-im/status-go/services/wallet/token"
walletToken "github.com/status-im/status-go/services/wallet/token"
"github.com/status-im/status-go/transactions" "github.com/status-im/status-go/transactions"
) )
@ -45,6 +46,7 @@ const (
Bridge Bridge
ERC721Transfer ERC721Transfer
ERC1155Transfer ERC1155Transfer
Swap
) )
func (s SendType) IsCollectiblesTransfer() bool { func (s SendType) IsCollectiblesTransfer() bool {
@ -96,7 +98,7 @@ func (s SendType) FindToken(service *Service, account common.Address, network *p
} }
func (s SendType) isTransfer() bool { func (s SendType) isTransfer() bool {
return s == Transfer || s.IsCollectiblesTransfer() return s == Transfer || s == Swap || s.IsCollectiblesTransfer()
} }
func (s SendType) needL1Fee() bool { func (s SendType) needL1Fee() bool {
@ -112,6 +114,10 @@ func (s SendType) isAvailableBetween(from, to *params.Network) bool {
return from.ChainID != to.ChainID return from.ChainID != to.ChainID
} }
if s == Swap {
return from.ChainID == to.ChainID
}
return true return true
} }
@ -136,11 +142,19 @@ func (s SendType) canUseBridge(b bridge.Bridge) bool {
} }
func (s SendType) isAvailableFor(network *params.Network) bool { func (s SendType) isAvailableFor(network *params.Network) bool {
if s == Swap {
return network.ChainID == walletCommon.EthereumMainnet ||
network.ChainID == walletCommon.OptimismMainnet ||
network.ChainID == walletCommon.ArbitrumMainnet
}
if s == Transfer || s == Bridge || s.IsCollectiblesTransfer() { if s == Transfer || s == Bridge || s.IsCollectiblesTransfer() {
return true return true
} }
if network.ChainID == 1 || network.ChainID == 5 || network.ChainID == 11155111 { if network.ChainID == walletCommon.EthereumMainnet ||
network.ChainID == walletCommon.EthereumGoerli ||
network.ChainID == walletCommon.EthereumSepolia {
return true return true
} }
@ -430,11 +444,13 @@ func NewRouter(s *Service) *Router {
erc1155Transfer := bridge.NewERC1155TransferBridge(s.rpcClient, s.transactor) erc1155Transfer := bridge.NewERC1155TransferBridge(s.rpcClient, s.transactor)
cbridge := bridge.NewCbridge(s.rpcClient, s.transactor, s.tokenManager) cbridge := bridge.NewCbridge(s.rpcClient, s.transactor, s.tokenManager)
hop := bridge.NewHopBridge(s.rpcClient, s.transactor, s.tokenManager) hop := bridge.NewHopBridge(s.rpcClient, s.transactor, s.tokenManager)
paraswap := bridge.NewSwapParaswap(s.rpcClient, s.transactor, s.tokenManager)
bridges[transfer.Name()] = transfer bridges[transfer.Name()] = transfer
bridges[erc721Transfer.Name()] = erc721Transfer bridges[erc721Transfer.Name()] = erc721Transfer
bridges[hop.Name()] = hop bridges[hop.Name()] = hop
bridges[cbridge.Name()] = cbridge bridges[cbridge.Name()] = cbridge
bridges[erc1155Transfer.Name()] = erc1155Transfer bridges[erc1155Transfer.Name()] = erc1155Transfer
bridges[paraswap.Name()] = paraswap
return &Router{s, bridges, s.rpcClient} return &Router{s, bridges, s.rpcClient}
} }
@ -455,55 +471,54 @@ type Router struct {
rpcClient *rpc.Client rpcClient *rpc.Client
} }
func (r *Router) requireApproval(ctx context.Context, sendType SendType, bridge bridge.Bridge, account common.Address, network *params.Network, token *token.Token, amountIn *big.Int) ( func (r *Router) requireApproval(ctx context.Context, sendType SendType, approvalContractAddress *common.Address, account common.Address, network *params.Network, token *token.Token, amountIn *big.Int) (
bool, *big.Int, uint64, uint64, *common.Address, error) { bool, *big.Int, uint64, uint64, error) {
if sendType.IsCollectiblesTransfer() { if sendType.IsCollectiblesTransfer() {
return false, nil, 0, 0, nil, nil return false, nil, 0, 0, nil
} }
if token.IsNative() { if token.IsNative() {
return false, nil, 0, 0, nil, nil return false, nil, 0, 0, nil
} }
contractMaker, err := contracts.NewContractMaker(r.rpcClient) contractMaker, err := contracts.NewContractMaker(r.rpcClient)
if err != nil { if err != nil {
return false, nil, 0, 0, nil, err return false, nil, 0, 0, err
}
bridgeAddress := bridge.GetContractAddress(network, token)
if bridgeAddress == nil {
return false, nil, 0, 0, nil, nil
} }
contract, err := contractMaker.NewERC20(network.ChainID, token.Address) contract, err := contractMaker.NewERC20(network.ChainID, token.Address)
if err != nil { if err != nil {
return false, nil, 0, 0, nil, err return false, nil, 0, 0, err
}
if approvalContractAddress == nil || *approvalContractAddress == bridge.ZeroAddress {
return false, nil, 0, 0, nil
} }
allowance, err := contract.Allowance(&bind.CallOpts{ allowance, err := contract.Allowance(&bind.CallOpts{
Context: ctx, Context: ctx,
}, account, *bridgeAddress) }, account, *approvalContractAddress)
if err != nil { if err != nil {
return false, nil, 0, 0, nil, err return false, nil, 0, 0, err
} }
if allowance.Cmp(amountIn) >= 0 { if allowance.Cmp(amountIn) >= 0 {
return false, nil, 0, 0, nil, nil return false, nil, 0, 0, nil
} }
ethClient, err := r.rpcClient.EthClient(network.ChainID) ethClient, err := r.rpcClient.EthClient(network.ChainID)
if err != nil { if err != nil {
return false, nil, 0, 0, nil, err return false, nil, 0, 0, err
} }
erc20ABI, err := abi.JSON(strings.NewReader(ierc20.IERC20ABI)) erc20ABI, err := abi.JSON(strings.NewReader(ierc20.IERC20ABI))
if err != nil { if err != nil {
return false, nil, 0, 0, nil, err return false, nil, 0, 0, err
} }
data, err := erc20ABI.Pack("approve", bridgeAddress, amountIn) data, err := erc20ABI.Pack("approve", approvalContractAddress, amountIn)
if err != nil { if err != nil {
return false, nil, 0, 0, nil, err return false, nil, 0, 0, err
} }
estimate, err := ethClient.EstimateGas(context.Background(), ethereum.CallMsg{ estimate, err := ethClient.EstimateGas(context.Background(), ethereum.CallMsg{
@ -513,25 +528,25 @@ func (r *Router) requireApproval(ctx context.Context, sendType SendType, bridge
Data: data, Data: data,
}) })
if err != nil { if err != nil {
return false, nil, 0, 0, nil, err return false, nil, 0, 0, err
} }
// fetching l1 fee // fetching l1 fee
var l1Fee uint64
oracleContractAddress, err := gaspriceoracle.ContractAddress(network.ChainID) oracleContractAddress, err := gaspriceoracle.ContractAddress(network.ChainID)
if err != nil { if err == nil {
return false, nil, 0, 0, nil, err oracleContract, err := gaspriceoracle.NewGaspriceoracleCaller(oracleContractAddress, ethClient)
if err != nil {
return false, nil, 0, 0, err
}
callOpt := &bind.CallOpts{}
l1FeeResult, _ := oracleContract.GetL1Fee(callOpt, data)
l1Fee = l1FeeResult.Uint64()
} }
oracleContract, err := gaspriceoracle.NewGaspriceoracleCaller(oracleContractAddress, ethClient) return true, amountIn, estimate, l1Fee, nil
if err != nil {
return false, nil, 0, 0, nil, err
}
callOpt := &bind.CallOpts{}
l1Fee, _ := oracleContract.GetL1Fee(callOpt, data)
return true, amountIn, estimate, l1Fee.Uint64(), bridgeAddress, nil
} }
func (r *Router) getBalance(ctx context.Context, network *params.Network, token *token.Token, account common.Address) (*big.Int, error) { func (r *Router) getBalance(ctx context.Context, network *params.Network, token *token.Token, account common.Address) (*big.Int, error) {
@ -574,6 +589,7 @@ func (r *Router) suggestedRoutes(
addrTo common.Address, addrTo common.Address,
amountIn *big.Int, amountIn *big.Int,
tokenID string, tokenID string,
toTokenID string,
disabledFromChainIDs, disabledFromChainIDs,
disabledToChaindIDs, disabledToChaindIDs,
preferedChainIDs []uint64, preferedChainIDs []uint64,
@ -599,6 +615,7 @@ func (r *Router) suggestedRoutes(
mu sync.Mutex mu sync.Mutex
candidates = make([]*Path, 0) candidates = make([]*Path, 0)
) )
for networkIdx := range networks { for networkIdx := range networks {
network := networks[networkIdx] network := networks[networkIdx]
if network.IsTest != areTestNetworksEnabled { if network.IsTest != areTestNetworksEnabled {
@ -618,6 +635,11 @@ func (r *Router) suggestedRoutes(
continue continue
} }
var toToken *walletToken.Token
if sendType == Swap {
toToken = sendType.FindToken(r.s, common.Address{}, network, toTokenID)
}
nativeToken := r.s.tokenManager.FindToken(network, network.NativeCurrencySymbol) nativeToken := r.s.tokenManager.FindToken(network, network.NativeCurrencySymbol)
if nativeToken == nil { if nativeToken == nil {
continue continue
@ -683,7 +705,7 @@ func (r *Router) suggestedRoutes(
continue continue
} }
can, err := bridge.Can(network, dest, token, maxAmountIn.ToInt()) can, err := bridge.Can(network, dest, token, toToken, maxAmountIn.ToInt())
if err != nil || !can { if err != nil || !can {
continue continue
} }
@ -704,7 +726,7 @@ func (r *Router) suggestedRoutes(
} }
gasLimit := uint64(0) gasLimit := uint64(0)
if sendType.isTransfer() { if sendType.isTransfer() {
gasLimit, err = bridge.EstimateGas(network, dest, addrFrom, addrTo, token, amountIn) gasLimit, err = bridge.EstimateGas(network, dest, addrFrom, addrTo, token, toToken, amountIn)
if err != nil { if err != nil {
continue continue
} }
@ -712,12 +734,13 @@ func (r *Router) suggestedRoutes(
gasLimit = sendType.EstimateGas(r.s, network, addrFrom, tokenID) gasLimit = sendType.EstimateGas(r.s, network, addrFrom, tokenID)
} }
approvalRequired, approvalAmountRequired, approvalGasLimit, l1ApprovalFee, approvalContractAddress, err := r.requireApproval(ctx, sendType, bridge, addrFrom, network, token, amountIn) approvalContractAddress := bridge.GetContractAddress(network, token)
approvalRequired, approvalAmountRequired, approvalGasLimit, l1ApprovalFee, err := r.requireApproval(ctx, sendType, approvalContractAddress, addrFrom, network, token, amountIn)
if err != nil { if err != nil {
continue continue
} }
var l1GasFeeWei uint64 = 0 var l1GasFeeWei uint64
if sendType.needL1Fee() { if sendType.needL1Fee() {
tx, err := bridge.BuildTx(network, addrFrom, addrTo, token, amountIn) tx, err := bridge.BuildTx(network, addrFrom, addrTo, token, amountIn)
if err != nil { if err != nil {
@ -727,6 +750,7 @@ func (r *Router) suggestedRoutes(
l1GasFeeWei, _ = r.s.feesManager.getL1Fee(ctx, network.ChainID, tx) l1GasFeeWei, _ = r.s.feesManager.getL1Fee(ctx, network.ChainID, tx)
l1GasFeeWei += l1ApprovalFee l1GasFeeWei += l1ApprovalFee
} }
gasFees.L1GasFee = weiToGwei(big.NewInt(int64(l1GasFeeWei))) gasFees.L1GasFee = weiToGwei(big.NewInt(int64(l1GasFeeWei)))
requiredNativeBalance := new(big.Int).Mul(gweiToWei(maxFees), big.NewInt(int64(gasLimit))) requiredNativeBalance := new(big.Int).Mul(gweiToWei(maxFees), big.NewInt(int64(gasLimit)))

View File

@ -0,0 +1,17 @@
package paraswap
type ClientV5 struct {
httpClient *HTTPClient
chainID uint64
}
func NewClientV5(chainID uint64) *ClientV5 {
return &ClientV5{
httpClient: NewHTTPClient(),
chainID: chainID,
}
}
func (c *ClientV5) SetChainID(chainID uint64) {
c.chainID = chainID
}

View File

@ -0,0 +1,77 @@
package paraswap
import (
"bytes"
"context"
"encoding/json"
"io/ioutil"
"net/http"
netUrl "net/url"
"time"
)
const requestTimeout = 5 * time.Second
type HTTPClient struct {
client *http.Client
}
func NewHTTPClient() *HTTPClient {
return &HTTPClient{
client: &http.Client{
Timeout: requestTimeout,
},
}
}
func (c *HTTPClient) doGetRequest(ctx context.Context, url string, params netUrl.Values) ([]byte, error) {
if len(params) > 0 {
url = url + "?" + params.Encode()
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
}
func (c *HTTPClient) doPostRequest(ctx context.Context, url string, params map[string]interface{}) ([]byte, error) {
jsonData, err := json.Marshal(params)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0")
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
}

View File

@ -0,0 +1,66 @@
package paraswap
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
)
const transactionsURL = "https://apiv5.paraswap.io/transactions/%d"
type Transaction struct {
From string `json:"from"`
To string `json:"to"`
Value string `json:"value"`
Data string `json:"data"`
GasPrice string `json:"gasPrice"`
Gas string `json:"gas"`
ChainID uint64 `json:"chainId"`
Error string `json:"error"`
}
func (c *ClientV5) BuildTransaction(ctx context.Context, srcTokenAddress common.Address, srcTokenDecimals uint, srcAmountWei *big.Int,
destTokenAddress common.Address, destTokenDecimals uint, destAmountWei *big.Int,
addressFrom common.Address, addressTo common.Address, priceRoute json.RawMessage) (Transaction, error) {
params := map[string]interface{}{}
params["srcToken"] = srcTokenAddress.Hex()
params["srcDecimals"] = srcTokenDecimals
params["srcAmount"] = srcAmountWei.String()
params["destToken"] = destTokenAddress.Hex()
params["destDecimals"] = destTokenDecimals
// params["destAmount"] = destAmountWei.String()
params["userAddress"] = addressFrom.Hex()
// params["receiver"] = addressTo.Hex() // at this point paraswap doesn't allow swap and transfer transaction
params["slippage"] = "500"
params["priceRoute"] = priceRoute
url := fmt.Sprintf(transactionsURL, c.chainID)
response, err := c.httpClient.doPostRequest(ctx, url, params)
if err != nil {
return Transaction{}, err
}
tx, err := handleBuildTransactionResponse(response)
if err != nil {
return Transaction{}, err
}
return tx, nil
}
func handleBuildTransactionResponse(response []byte) (Transaction, error) {
var transactionResponse Transaction
err := json.Unmarshal(response, &transactionResponse)
if err != nil {
return Transaction{}, err
}
if transactionResponse.Error != "" {
return Transaction{}, errors.New(transactionResponse.Error)
}
return transactionResponse, nil
}

View File

@ -0,0 +1,43 @@
package paraswap
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestUnmarshallBuildTransaction(t *testing.T) {
tx := Transaction{
From: "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8",
To: "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57",
Value: "10000000000000000",
Data: "0xf566103400000000000000000000000075e48c954594d64ef9613aeef97ad85370f13807b2b53dca60cae1d1f93f64d80703b888689f28b63c483459183f2f4271fa0308000000000000000000000000000000000000000000000000002386f26fc100000000000000000000000000000000000000000000000000000000000001c2354900000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7",
GasPrice: "47490307239",
Gas: "197142",
ChainID: 1,
}
data := []byte(`{
"from": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8",
"to": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57",
"value": "10000000000000000",
"data": "0xf566103400000000000000000000000075e48c954594d64ef9613aeef97ad85370f13807b2b53dca60cae1d1f93f64d80703b888689f28b63c483459183f2f4271fa0308000000000000000000000000000000000000000000000000002386f26fc100000000000000000000000000000000000000000000000000000000000001c2354900000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7",
"gasPrice": "47490307239",
"gas": "197142",
"chainId": 1
}`)
receivedTx, err := handleBuildTransactionResponse(data)
assert.NoError(t, err)
assert.Equal(t, tx, receivedTx)
}
func TestForErrorOnBuildingTransaction(t *testing.T) {
data := []byte(`{
"error": "Invalid tokens"
}`)
_, err := handleBuildTransactionResponse(data)
assert.Error(t, err)
}

View File

@ -0,0 +1,78 @@
package paraswap
import (
"context"
"encoding/json"
"errors"
"math/big"
netUrl "net/url"
"strconv"
"github.com/ethereum/go-ethereum/common"
"github.com/status-im/status-go/services/wallet/bigint"
)
const pricesURL = "https://apiv5.paraswap.io/prices"
type Route struct {
GasCost *bigint.BigInt `json:"gasCost"`
SrcAmount *bigint.BigInt `json:"srcAmount"`
SrcTokenAddress common.Address `json:"srcToken"`
SrcTokenDecimals uint `json:"srcDecimals"`
DestAmount *bigint.BigInt `json:"destAmount"`
DestTokenAddress common.Address `json:"destToken"`
DestTokenDecimals uint `json:"destDecimals"`
RawPriceRoute json.RawMessage `json:"rawPriceRoute"`
}
type PriceRouteResponse struct {
PriceRoute json.RawMessage `json:"priceRoute"`
Error string `json:"error"`
}
func (c *ClientV5) FetchPriceRoute(ctx context.Context, srcTokenAddress common.Address, srcTokenDecimals uint,
destTokenAddress common.Address, destTokenDecimals uint, amountWei *big.Int, addressFrom common.Address,
addressTo common.Address) (Route, error) {
params := netUrl.Values{}
params.Add("srcToken", srcTokenAddress.Hex())
params.Add("srcDecimals", strconv.Itoa(int(srcTokenDecimals)))
params.Add("destToken", destTokenAddress.Hex())
params.Add("destDecimals", strconv.Itoa(int(destTokenDecimals)))
params.Add("userAddress", addressFrom.Hex())
// params.Add("receiver", addressTo.Hex()) // at this point paraswap doesn't allow swap and transfer transaction
params.Add("network", strconv.FormatUint(c.chainID, 10))
params.Add("amount", amountWei.String())
params.Add("side", "SELL")
url := pricesURL
response, err := c.httpClient.doGetRequest(ctx, url, params)
if err != nil {
return Route{}, err
}
return handlePriceRouteResponse(response)
}
func handlePriceRouteResponse(response []byte) (Route, error) {
var priceRouteResponse PriceRouteResponse
err := json.Unmarshal(response, &priceRouteResponse)
if err != nil {
return Route{}, err
}
if priceRouteResponse.Error != "" {
return Route{}, errors.New(priceRouteResponse.Error)
}
var route Route
err = json.Unmarshal(priceRouteResponse.PriceRoute, &route)
if err != nil {
return Route{}, err
}
route.RawPriceRoute = priceRouteResponse.PriceRoute
return route, nil
}

View File

@ -0,0 +1,126 @@
package paraswap
import (
"fmt"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/assert"
"github.com/status-im/status-go/services/wallet/bigint"
)
func TestUnmarshallPriceRoute(t *testing.T) {
data := []byte(`{
"blockNumber": 13015909,
"network": 1,
"srcToken": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
"srcDecimals": 18,
"srcAmount": "1000000000000000000",
"destToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"destDecimals": 18,
"destAmount": "1000000000000000000",
"bestRoute": {
"percent": 100,
"swaps": [
{
"srcToken": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
"srcDecimals": 0,
"destToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"destDecimals": 0,
"swapExchanges": [
{
"exchange": "UniswapV2",
"srcAmount": "1000000000000000000",
"destAmount": "1000000000000000000",
"percent": 100,
"data": {
"router": "0x0000000000000000000000000000000000000000",
"path": [
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
],
"factory": "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f",
"initCode": "0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f",
"feeFactor": 10000,
"pools": [
{
"address": "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc",
"fee": 30,
"direction": false
}
],
"gasUSD": "13.227195"
}
}
]
}
]
},
"others": {
"exchange": "UniswapV2",
"srcAmount": "1000000000000000000",
"destAmount": "3255989380",
"unit": "3255989380",
"data": {
"router": "0x0000000000000000000000000000000000000000",
"path": [
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
],
"factory": "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f",
"initCode": "0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f",
"feeFactor": 10000,
"pools": [
{
"address": "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc",
"fee": 30,
"direction": false
}
],
"gasUSD": "13.227195"
}
},
"gasCostUSD": "11.947163",
"gasCost": "111435",
"side": "SELL",
"tokenTransferProxy": "0x3e7d31751347BAacf35945074a4a4A41581B2271",
"contractAddress": "0x485D2446711E141D2C8a94bC24BeaA5d5A110D74",
"contractMethod": "swapOnUniswap",
"srcUSD": "3230.3000000000",
"destUSD": "3218.9300566052",
"partner": "paraswap.io",
"partnerFee": 0,
"maxImpactReached": false,
"hmac": "319c5cf83098a07aeebb11bed6310db51311201f"
}`)
responseData := []byte(fmt.Sprintf(`{"priceRoute":%s}`, string(data)))
route := Route{
GasCost: &bigint.BigInt{Int: big.NewInt(111435)},
SrcAmount: &bigint.BigInt{Int: big.NewInt(1000000000000000000)},
SrcTokenAddress: common.HexToAddress("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"),
SrcTokenDecimals: 18,
DestAmount: &bigint.BigInt{Int: big.NewInt(1000000000000000000)},
DestTokenAddress: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
DestTokenDecimals: 18,
RawPriceRoute: data,
}
receivedRoute, err := handlePriceRouteResponse(responseData)
assert.NoError(t, err)
assert.Equal(t, route, receivedRoute)
}
func TestForErrorOnFetchingPriceRoute(t *testing.T) {
data := []byte(`{
"error": "Invalid tokens"
}`)
_, err := handlePriceRouteResponse(data)
assert.Error(t, err)
}

View File

@ -0,0 +1,47 @@
package paraswap
import (
"context"
"encoding/json"
"errors"
"fmt"
)
const tokensURL = "https://apiv5.paraswap.io/tokens/%d" // nolint: gosec
type Token struct {
Symbol string `json:"symbol"`
Address string `json:"address"`
Decimals uint `json:"decimals"`
Img string `json:"img"`
Network int `json:"network"`
}
type TokensResponse struct {
Tokens []Token `json:"tokens"`
Error string `json:"error"`
}
func (c *ClientV5) FetchTokensList(ctx context.Context) ([]Token, error) {
url := fmt.Sprintf(tokensURL, c.chainID)
response, err := c.httpClient.doGetRequest(ctx, url, nil)
if err != nil {
return nil, err
}
return handleTokensListResponse(response)
}
func handleTokensListResponse(response []byte) ([]Token, error) {
var tokensResponse TokensResponse
err := json.Unmarshal(response, &tokensResponse)
if err != nil {
return nil, err
}
if tokensResponse.Error != "" {
return nil, errors.New(tokensResponse.Error)
}
return tokensResponse.Tokens, nil
}

View File

@ -0,0 +1,59 @@
package paraswap
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestUnmarshallTokensList(t *testing.T) {
tokens := []Token{
{
Symbol: "ETH",
Address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
Decimals: 18,
Img: "https://img.paraswap.network/ETH.png",
Network: 1,
},
{
Symbol: "USDT",
Address: "0xdac17f958d2ee523a2206206994597c13d831ec7",
Decimals: 6,
Img: "https://img.paraswap.network/USDT.png",
Network: 1,
},
}
data := []byte(`{
"tokens": [
{
"symbol": "ETH",
"address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
"decimals": 18,
"img": "https://img.paraswap.network/ETH.png",
"network": 1
},
{
"symbol": "USDT",
"address": "0xdac17f958d2ee523a2206206994597c13d831ec7",
"decimals": 6,
"img": "https://img.paraswap.network/USDT.png",
"network": 1
}
]
}`)
receivedTokens, err := handleTokensListResponse(data)
assert.NoError(t, err)
assert.Equal(t, tokens, receivedTokens)
}
func TestForErrorOnFetchingTokensList(t *testing.T) {
data := []byte(`{
"error": "Only chainId 1 is supported"
}`)
_, err := handleTokensListResponse(data)
assert.Error(t, err)
}

View File

@ -72,7 +72,7 @@ type ReceivedToken struct {
} }
func (t *Token) IsNative() bool { func (t *Token) IsNative() bool {
return t.Address == nativeChainAddress return strings.EqualFold(t.Symbol, "ETH")
} }
type List struct { type List struct {

View File

@ -347,6 +347,10 @@ func (tm *TransactionManager) sendTransactions(multiTransaction *MultiTransactio
tx.ERC1155TransferTx.MultiTransactionID = multiTransaction.ID tx.ERC1155TransferTx.MultiTransactionID = multiTransaction.ID
tx.ERC1155TransferTx.Symbol = multiTransaction.FromAsset tx.ERC1155TransferTx.Symbol = multiTransaction.FromAsset
} }
if tx.SwapTx != nil {
tx.SwapTx.MultiTransactionID = multiTransaction.ID
tx.SwapTx.Symbol = multiTransaction.FromAsset
}
hash, err := bridges[tx.BridgeName].Send(tx, selectedAccount) hash, err := bridges[tx.BridgeName].Send(tx, selectedAccount)
if err != nil { if err != nil {