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) {
addr, exists := contractAddressByChainID[chainID]
if !exists {
return *new(common.Address), ErrorNotAvailableOnChainID
return common.Address{}, ErrorNotAvailableOnChainID
}
return addr, nil
}

View File

@ -426,6 +426,7 @@ func (api *API) GetSuggestedRoutes(
addrTo common.Address,
amountIn *hexutil.Big,
tokenID string,
toTokenID string,
disabledFromChainIDs,
disabledToChaindIDs,
preferedChainIDs []uint64,
@ -433,7 +434,7 @@ func (api *API) GetSuggestedRoutes(
fromLockedAmount map[uint64]*hexutil.Big,
) (*SuggestedRoutes, error) {
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)
}

View File

@ -14,6 +14,8 @@ import (
"github.com/status-im/status-go/transactions"
)
var ZeroAddress = common.Address{}
const IncreaseEstimatedGasFactor = 1.1
func getSigner(chainID uint64, from types.Address, verifiedAccount *account.SelectedExtKey) bind.SignerFn {
@ -31,6 +33,7 @@ type TransactionBridge struct {
CbridgeTx *CBridgeTxArgs
ERC721TransferTx *ERC721TransferTxArgs
ERC1155TransferTx *ERC1155TransferTxArgs
SwapTx *SwapTxArgs
}
func (t *TransactionBridge) Value() *big.Int {
@ -99,9 +102,9 @@ func (t *TransactionBridge) Data() types.HexBytes {
type Bridge interface {
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)
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)
Send(sendArgs *TransactionBridge, verifiedAccount *account.SelectedExtKey) (types.Hash, error)
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
}
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 {
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
}
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
value := new(big.Int)

View File

@ -41,7 +41,7 @@ func (s *ERC1155TransferBridge) Name() string {
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
}
@ -49,7 +49,7 @@ func (s *ERC1155TransferBridge) CalculateFees(from, to *params.Network, token *t
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)
if err != nil {
return 0, err

View File

@ -40,7 +40,7 @@ func (s *ERC721TransferBridge) Name() string {
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
}
@ -48,7 +48,7 @@ func (s *ERC721TransferBridge) CalculateFees(from, to *params.Network, token *to
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)
if err != nil {
return 0, err

View File

@ -108,7 +108,7 @@ func (h *HopBridge) Name() string {
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 {
return false, nil
}
@ -132,7 +132,7 @@ func (h *HopBridge) Can(from, to *params.Network, token *token.Token, balance *b
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
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"
}
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
}
@ -40,7 +40,7 @@ func (s *TransferBridge) CalculateFees(from, to *params.Network, token *token.To
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)
var err error
if token.Symbol == "ETH" {

View File

@ -26,6 +26,7 @@ import (
"github.com/status-im/status-go/services/wallet/bridge"
walletCommon "github.com/status-im/status-go/services/wallet/common"
"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"
)
@ -45,6 +46,7 @@ const (
Bridge
ERC721Transfer
ERC1155Transfer
Swap
)
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 {
return s == Transfer || s.IsCollectiblesTransfer()
return s == Transfer || s == Swap || s.IsCollectiblesTransfer()
}
func (s SendType) needL1Fee() bool {
@ -112,6 +114,10 @@ func (s SendType) isAvailableBetween(from, to *params.Network) bool {
return from.ChainID != to.ChainID
}
if s == Swap {
return from.ChainID == to.ChainID
}
return true
}
@ -136,11 +142,19 @@ func (s SendType) canUseBridge(b bridge.Bridge) 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() {
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
}
@ -430,11 +444,13 @@ func NewRouter(s *Service) *Router {
erc1155Transfer := bridge.NewERC1155TransferBridge(s.rpcClient, s.transactor)
cbridge := bridge.NewCbridge(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[erc721Transfer.Name()] = erc721Transfer
bridges[hop.Name()] = hop
bridges[cbridge.Name()] = cbridge
bridges[erc1155Transfer.Name()] = erc1155Transfer
bridges[paraswap.Name()] = paraswap
return &Router{s, bridges, s.rpcClient}
}
@ -455,55 +471,54 @@ type Router struct {
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) (
bool, *big.Int, uint64, uint64, *common.Address, error) {
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, error) {
if sendType.IsCollectiblesTransfer() {
return false, nil, 0, 0, nil, nil
return false, nil, 0, 0, nil
}
if token.IsNative() {
return false, nil, 0, 0, nil, nil
return false, nil, 0, 0, nil
}
contractMaker, err := contracts.NewContractMaker(r.rpcClient)
if err != nil {
return false, nil, 0, 0, nil, err
}
bridgeAddress := bridge.GetContractAddress(network, token)
if bridgeAddress == nil {
return false, nil, 0, 0, nil, nil
return false, nil, 0, 0, err
}
contract, err := contractMaker.NewERC20(network.ChainID, token.Address)
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{
Context: ctx,
}, account, *bridgeAddress)
}, account, *approvalContractAddress)
if err != nil {
return false, nil, 0, 0, nil, err
return false, nil, 0, 0, err
}
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)
if err != nil {
return false, nil, 0, 0, nil, err
return false, nil, 0, 0, err
}
erc20ABI, err := abi.JSON(strings.NewReader(ierc20.IERC20ABI))
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 {
return false, nil, 0, 0, nil, err
return false, nil, 0, 0, err
}
estimate, err := ethClient.EstimateGas(context.Background(), ethereum.CallMsg{
@ -513,25 +528,25 @@ func (r *Router) requireApproval(ctx context.Context, sendType SendType, bridge
Data: data,
})
if err != nil {
return false, nil, 0, 0, nil, err
return false, nil, 0, 0, err
}
// fetching l1 fee
var l1Fee uint64
oracleContractAddress, err := gaspriceoracle.ContractAddress(network.ChainID)
if err != nil {
return false, nil, 0, 0, nil, err
if err == nil {
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)
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
return true, amountIn, estimate, l1Fee, nil
}
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,
amountIn *big.Int,
tokenID string,
toTokenID string,
disabledFromChainIDs,
disabledToChaindIDs,
preferedChainIDs []uint64,
@ -599,6 +615,7 @@ func (r *Router) suggestedRoutes(
mu sync.Mutex
candidates = make([]*Path, 0)
)
for networkIdx := range networks {
network := networks[networkIdx]
if network.IsTest != areTestNetworksEnabled {
@ -618,6 +635,11 @@ func (r *Router) suggestedRoutes(
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)
if nativeToken == nil {
continue
@ -683,7 +705,7 @@ func (r *Router) suggestedRoutes(
continue
}
can, err := bridge.Can(network, dest, token, maxAmountIn.ToInt())
can, err := bridge.Can(network, dest, token, toToken, maxAmountIn.ToInt())
if err != nil || !can {
continue
}
@ -704,7 +726,7 @@ func (r *Router) suggestedRoutes(
}
gasLimit := uint64(0)
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 {
continue
}
@ -712,12 +734,13 @@ func (r *Router) suggestedRoutes(
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 {
continue
}
var l1GasFeeWei uint64 = 0
var l1GasFeeWei uint64
if sendType.needL1Fee() {
tx, err := bridge.BuildTx(network, addrFrom, addrTo, token, amountIn)
if err != nil {
@ -727,6 +750,7 @@ func (r *Router) suggestedRoutes(
l1GasFeeWei, _ = r.s.feesManager.getL1Fee(ctx, network.ChainID, tx)
l1GasFeeWei += l1ApprovalFee
}
gasFees.L1GasFee = weiToGwei(big.NewInt(int64(l1GasFeeWei)))
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 {
return t.Address == nativeChainAddress
return strings.EqualFold(t.Symbol, "ETH")
}
type List struct {

View File

@ -347,6 +347,10 @@ func (tm *TransactionManager) sendTransactions(multiTransaction *MultiTransactio
tx.ERC1155TransferTx.MultiTransactionID = multiTransaction.ID
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)
if err != nil {