From 82a6a97eb5769ebcc24988a3d945e1e117b0fce7 Mon Sep 17 00:00:00 2001 From: perror <23651751+perrornet@users.noreply.github.com> Date: Tue, 20 Aug 2024 01:04:57 +0800 Subject: [PATCH] Add Bungee --- internal/daemons/market/market.go | 2 +- utils/constant/chain.go | 92 ++++---- utils/provider/bridge/bungee/bungee.go | 281 ++++++++++++++++++++++++ utils/provider/bridge/bungee/init.go | 10 + utils/provider/bridge/bungee/utils.go | 289 +++++++++++++++++++++++++ utils/provider/bridge/bungee/vars.go | 183 ++++++++++++++++ utils/util.go | 9 + 7 files changed, 821 insertions(+), 45 deletions(-) create mode 100644 utils/provider/bridge/bungee/bungee.go create mode 100644 utils/provider/bridge/bungee/init.go create mode 100644 utils/provider/bridge/bungee/utils.go create mode 100644 utils/provider/bridge/bungee/vars.go diff --git a/internal/daemons/market/market.go b/internal/daemons/market/market.go index 50a5daa..5768cf9 100644 --- a/internal/daemons/market/market.go +++ b/internal/daemons/market/market.go @@ -252,7 +252,7 @@ func processOrder(ctx context.Context, order models.Order, conf configs.Config) return nil } if result.Status == "" { - return errors.New("the result status is empty") + return errors.Errorf("the result status is empty: %v", providerErr) } if result.CurrentChain != args.TargetChain && providerErr == nil { result.Status = models.OrderStatusWaitCrossChain diff --git a/utils/constant/chain.go b/utils/constant/chain.go index fd709a6..da2807d 100644 --- a/utils/constant/chain.go +++ b/utils/constant/chain.go @@ -3,38 +3,40 @@ package constant import "golang.org/x/exp/constraints" const ( - Zksync = "zksync" - Sepolia = "sepolia" - Polygon = "polygon" - PolygonZkEvm = "polygon-zkEvm" - Bsc = "bsc" - Blast = "blast" - DarwiniaDvm = "darwinia-dvm" - Base = "base" Arbitrum = "arbitrum" - Gnosis = "gnosis" - Scroll = "scroll" - Optimism = "op" + ArbitrumSepolia = "arbitrum-sepolia" + Astar = "astar" AstarZkevm = "astar-zkevm" - Mantle = "mantle" - Linea = "linea" + Aurora = "aurora" + Avalanche = "avalanche" + Base = "base" + Blast = "blast" + Bnb = "bnb" + Bsc = "bsc" + Celo = "celo" CrabDvm = "crab-dvm" + Cronos = "cronos" + DarwiniaDvm = "darwinia-dvm" Ethereum = "ethereum" + Fantom = "fantom" + Gnosis = "gnosis" + Kava = "kava" + Linea = "linea" + Mantle = "mantle" Merlin = "merlin" - Avalanche = "avalanche" Mode = "mode" - Cronos = "cronos" + Moonbeam = "moonbeam" + OpBNB = "opBNB" + Optimism = "op" + Polygon = "polygon" + PolygonZkEvm = "polygon-zkEvm" PulseChain = "pulseChain" - Kava = "kava" - ZkLinkNova = "zkLink-nova" Rootstock = "rootstock" - Astar = "astar" - OpBNB = "opBNB" - Bnb = "bnb" - Celo = "celo" - Moonbeam = "moonbeam" - ArbitrumSepolia = "arbitrum-sepolia" + Scroll = "scroll" + Sepolia = "sepolia" Zircuit = "zircuit" + ZkLinkNova = "zkLink-nova" + Zksync = "zksync" ) const ( @@ -46,37 +48,39 @@ const ( var ( chainName2Id = map[string]int{ + Aurora: 1313161554, + Arbitrum: 42161, ArbitrumSepolia: 421614, - Moonbeam: 1284, - Sepolia: 11155111, - Zksync: 324, - Polygon: 137, - PolygonZkEvm: 1101, - Bsc: 56, + Astar: 592, + AstarZkevm: 3776, + Avalanche: 43114, + Base: 8453, Blast: 81457, + Bsc: 56, + Celo: 42220, + CrabDvm: 44, + Cronos: 25, DarwiniaDvm: 46, - Base: 8453, - Arbitrum: 42161, + Ethereum: 1, + Fantom: 250, Gnosis: 100, - Scroll: 534352, - Optimism: 10, - AstarZkevm: 3776, - Mantle: 5000, + Kava: 2222, Linea: 59144, - CrabDvm: 44, - Ethereum: 1, + Mantle: 5000, Merlin: 4200, - Avalanche: 43114, Mode: 34443, - Cronos: 25, + Moonbeam: 1284, + OpBNB: 204, + Optimism: 10, + Polygon: 137, + PolygonZkEvm: 1101, PulseChain: 369, - Kava: 2222, - ZkLinkNova: 810180, Rootstock: 30, - Astar: 592, - OpBNB: 204, - Celo: 42220, + Scroll: 534352, + Sepolia: 11155111, Zircuit: 48900, + ZkLinkNova: 810180, + Zksync: 324, } chainId2Name = make(map[int]string) ) diff --git a/utils/provider/bridge/bungee/bungee.go b/utils/provider/bridge/bungee/bungee.go new file mode 100644 index 0000000..f1cb5ff --- /dev/null +++ b/utils/provider/bridge/bungee/bungee.go @@ -0,0 +1,281 @@ +package bungee + +import ( + "context" + "math/big" + "omni-balance/utils/chains" + "omni-balance/utils/configs" + "omni-balance/utils/constant" + "omni-balance/utils/error_types" + "omni-balance/utils/provider" + + log "omni-balance/utils/logging" + + "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + "github.com/shopspring/decimal" + "github.com/tidwall/gjson" +) + +func init() { + provider.Register(configs.Bridge, New) +} + +type Bungee struct { + conf configs.Config +} + +func New(conf configs.Config, _ ...bool) (provider.Provider, error) { + return Bungee{ + conf: conf, + }, nil +} + +func (r Bungee) GetCost(ctx context.Context, args provider.SwapParams) (provider.TokenInCosts, error) { + var ( + err error + costAmount decimal.Decimal + ) + if _, ok := SupportedChain[args.TargetChain]; !ok { + return nil, error_types.ErrUnsupportedTokenAndChain + } + + if args.SourceChain == "" || args.SourceToken == "" { + var quote gjson.Result + args.SourceToken, args.SourceChain, costAmount, quote, err = r.GetBestQuote(ctx, args) + if err != nil { + return nil, err + } + if len(quote.Get("result.routes").Array()) == 0 { + return nil, error_types.ErrUnsupportedTokenAndChain + } + } + + if args.SourceChain == "" || args.SourceToken == "" || costAmount.IsZero() { + return nil, error_types.ErrUnsupportedTokenAndChain + } + return provider.TokenInCosts{ + provider.TokenInCost{ + TokenName: args.SourceToken, + CostAmount: costAmount, + }, + }, nil +} + +func (r Bungee) CheckToken(ctx context.Context, tokenName, tokenInChainName, tokenOutChainName string, amount decimal.Decimal) (bool, error) { + var ( + tokenIn = r.conf.GetTokenInfoOnChain(tokenName, tokenInChainName) + tokenOut = r.conf.GetTokenInfoOnChain(tokenName, tokenOutChainName) + tokenInChain = r.conf.GetChainConfig(tokenInChainName) + tokenOuChain = r.conf.GetChainConfig(tokenOutChainName) + ) + + if _, ok := SupportedChain[tokenOuChain.Name]; !ok { + return false, error_types.ErrUnsupportedTokenAndChain + } + + if _, ok := SupportedChain[tokenInChain.Name]; !ok { + return false, error_types.ErrUnsupportedTokenAndChain + } + + quote, err := r.Quote(ctx, QuoteParams{ + FromTokenAddress: common.HexToAddress(tokenIn.ContractAddress), + ToTokenAddress: common.HexToAddress(tokenOut.ContractAddress), + AmountWei: decimal.NewFromBigInt(chains.EthToWei(amount, tokenIn.Decimals), 0), + FromTokenChainId: tokenInChain.Id, + ToTokenChainId: tokenOuChain.Id, + }) + if len(quote.Get("result").Get("routes").Array()) == 0 { + return false, error_types.ErrUnsupportedTokenAndChain + } + return err == nil, err +} + +func (r Bungee) Swap(ctx context.Context, args provider.SwapParams) (provider.SwapResult, error) { + var ( + err error + targetChain = r.conf.GetChainConfig(args.TargetChain) + tokenOut = r.conf.GetTokenInfoOnChain(args.TargetToken, targetChain.Name) + tokenOutAmount = args.Amount + tokenOutAmountWei = decimal.NewFromBigInt(chains.EthToWei(tokenOutAmount, tokenOut.Decimals), 0) + tokenIn configs.Token + tokenInAmount decimal.Decimal + tokenInAmountWei decimal.Decimal + costAmount decimal.Decimal + history = args.LastHistory + tx = history.Tx + actionNumber = Action2Int(history.Actions) + isActionSuccess = history.Status == provider.TxStatusSuccess.String() + sourceChain configs.Chain + quote gjson.Result + ) + + args.SourceToken, args.SourceChain, costAmount, _, err = r.GetBestQuote(ctx, args) + if err != nil { + return provider.SwapResult{}, err + } + + if args.SourceChain == "" || args.SourceToken == "" { + log.Fatalf("#%d %s source chain and token is required", args.OrderId, args.TargetToken) + } + tokenIn = r.conf.GetTokenInfoOnChain(args.SourceToken, args.SourceChain) + sourceChain = r.conf.GetChainConfig(args.SourceChain) + tokenInAmount = costAmount + tokenInAmountWei = decimal.NewFromBigInt(chains.EthToWei(tokenInAmount, tokenIn.Decimals), 0) + isTokenInNative := r.conf.IsNativeToken(sourceChain.Name, tokenIn.Name) + + quotes, err := r.Quote(ctx, QuoteParams{ + FromTokenAddress: common.HexToAddress(tokenIn.ContractAddress), + ToTokenAddress: common.HexToAddress(tokenOut.ContractAddress), + AmountWei: decimal.NewFromBigInt(chains.EthToWei(costAmount, tokenIn.Decimals), 0), + FromTokenChainId: sourceChain.Id, + ToTokenChainId: constant.GetChainId(args.TargetChain), + }) + if err != nil { + return provider.SwapResult{}, err + } + + if len(quotes.Get("result").Get("routes").Array()) == 0 { + return provider.SwapResult{}, error_types.ErrUnsupportedTokenAndChain + } + quote = quotes.Get("result").Get("routes").Array()[0] + tokenOutAmountWei = decimal.RequireFromString(quote.Get("toAmount").String()) + tokenOutAmount = chains.WeiToEth(tokenOutAmountWei.BigInt(), tokenOut.Decimals) + args.Amount = tokenOutAmount + if tokenOutAmount.LessThanOrEqual(decimal.Zero) { + return provider.SwapResult{}, errors.New("token out amount is zero") + } + + ctx = context.WithValue(ctx, constant.ChainNameKeyInCtx, sourceChain.Name) + client, err := chains.NewTryClient(ctx, sourceChain.RpcEndpoints) + if err != nil { + return provider.SwapResult{}, err + } + var ( + sr = new(provider.SwapResult). + SetTokenInName(tokenIn.Name). + SetTokenInChainName(args.SourceChain). + SetProviderName(r.Name()). + SetProviderType(r.Type()). + SetCurrentChain(args.SourceChain). + SetTx(args.LastHistory.Tx). + SetReciever(args.Receiver) + sh = &provider.SwapHistory{ + ProviderName: r.Name(), + ProviderType: string(r.Type()), + Amount: args.Amount, + CurrentChain: args.SourceChain, + Tx: history.Tx, + } + ) + + if !isTokenInNative && actionNumber <= 1 && !isActionSuccess { + log.Debugf("#%d %s is not native token, need approve", args.OrderId, tokenIn.Name) + args.RecordFn(sh.SetActions(ApproveTransactionAction).SetStatus(provider.TxStatusPending).Out()) + spender := quote.Get("userTxs.0.approvalData.allowanceTarget").String() + if spender == "" { + return provider.SwapResult{}, errors.New("spender is empty") + } + err = chains.TokenApprove(ctx, + chains.TokenApproveParams{ + ChainId: int64(sourceChain.Id), + TokenAddress: common.HexToAddress(tokenIn.ContractAddress), + Owner: args.Sender.GetAddress(true), + SendTransaction: args.Sender.SendTransaction, + WaitTransaction: args.Sender.WaitTransaction, + Spender: common.HexToAddress(spender), + AmountWei: tokenInAmountWei.Mul(decimal.RequireFromString("1.02")), + IsNotWaitTx: r.conf.GetWalletConfig(string(args.Sender.GetAddress().Hex())).MultiSignType != "", + Client: client, + }) + if err != nil { + args.RecordFn(sh.SetActions(ApproveTransactionAction).SetStatus(provider.TxStatusFailed).Out(), err) + return sr.SetError(err).SetStatus(provider.TxStatusFailed).Out(), err + } + log.Debugf("#%d %s approve transaction success", args.OrderId, tokenIn.Name) + args.RecordFn(sh.SetActions(ApproveTransactionAction).SetStatus(provider.TxStatusSuccess).Out()) + } + + if actionNumber <= 2 && (!isActionSuccess || actionNumber == 1) { + amount := args.Amount.Copy() + args.Amount = tokenInAmount + ctx = provider.WithNotify(ctx, provider.WithNotifyParams{ + OrderId: args.OrderId, + Receiver: common.HexToAddress(args.Receiver), + TokenIn: tokenIn.Name, + TokenOut: tokenOut.Name, + TokenInChain: args.SourceChain, + TokenOutChain: args.TargetChain, + ProviderName: r.Name(), + TokenInAmount: tokenInAmount, + TokenOutAmount: tokenOutAmount, + TransactionType: provider.SwapTransactionAction, + }) + + buildTx, err := r.BuildTx(ctx, quote, args.Sender.GetAddress(true), common.HexToAddress(args.Receiver)) + if err != nil { + return sr.SetError(err).SetStatus(provider.TxStatusFailed).Out(), errors.Wrap(err, "build tx error") + } + args.Amount = amount + if !isTokenInNative && buildTx.Value.Cmp(big.NewInt(0)) != 0 { + err = errors.Errorf("tokenin is not native token, but value is not zero") + return sr.SetError(err).SetStatus(provider.TxStatusFailed).Out(), err + } + if isTokenInNative && buildTx.Value.Cmp(big.NewInt(0)) == 0 { + err = errors.Errorf("tokenin is native token, but value is zero") + return sr.SetError(err).SetStatus(provider.TxStatusFailed).Out(), err + } + if isTokenInNative && decimal.NewFromBigInt(buildTx.Value, 0).GreaterThan(tokenInAmountWei.Mul(decimal.RequireFromString("1.5"))) { + err = errors.Errorf("tx value is too high, tx value: %s, amount: %s", buildTx.Value.String(), tokenInAmountWei.String()) + return sr.SetError(err).SetStatus(provider.TxStatusFailed).Out(), err + } + sr.SetOrder(buildTx) + args.RecordFn(sh.SetActions(SourceChainSendingAction).SetStatus(provider.TxStatusPending).Out()) + txHash, err := args.Sender.SendTransaction(ctx, buildTx, client) + if err != nil { + args.RecordFn(sh.SetActions(SourceChainSendingAction).SetStatus(provider.TxStatusFailed).Out(), err) + return sr.SetError(err).SetStatus(provider.TxStatusFailed).Out(), errors.Wrap(err, "send tx error") + } + log.Debugf("#%d %s sending tx on chain success", args.OrderId, tokenIn.Name) + sh = sh.SetTx(txHash.Hex()) + sr = sr.SetTx(txHash.Hex()).SetOrderId(txHash.Hex()) + args.RecordFn(sh.SetActions(SourceChainSendingAction).SetStatus(provider.TxStatusSuccess).Out()) + tx = txHash.Hex() + } + + args.RecordFn(sh.SetActions(WaitForTxAction).SetStatus(provider.TxStatusPending).Out()) + log.Debugf("#%d %s waiting for tx on chain", args.OrderId, tokenIn.Name) + if err := args.Sender.WaitTransaction(ctx, common.HexToHash(tx), client); err != nil { + args.RecordFn(sh.SetActions(WaitForTxAction).SetStatus(provider.TxStatusFailed).Out(), err) + return sr.SetError(err).SetStatus(provider.TxStatusFailed).Out(), errors.Wrap(err, "wait tx error") + } + realHash, err := args.Sender.GetRealHash(ctx, common.HexToHash(tx), client) + if err != nil { + args.RecordFn(sh.SetActions(WaitForTxAction).SetStatus(provider.TxStatusFailed).Out(), err) + return sr.SetError(err).SetStatus(provider.TxStatusFailed).Out(), errors.Wrap(err, "get real hash error") + } + log.Debugf("#%d %s waiting for tx in router nitro", args.OrderId, tokenIn.Name) + if err := r.WaitForTx(ctx, sourceChain.Id, targetChain.Id, realHash); err != nil { + args.RecordFn(sh.SetActions(WaitForTxAction).SetStatus(provider.TxStatusFailed).Out(), err) + return sr.SetError(err).SetStatus(provider.TxStatusFailed).Out(), errors.Wrap(err, "wait router nitro error") + } + log.Debugf("#%d %s waiting for tx success in routernitro", args.OrderId, tokenIn.Name) + args.RecordFn(sh.SetActions(WaitForTxAction).SetStatus(provider.TxStatusSuccess).SetCurrentChain(targetChain.Name).Out()) + return sr.SetStatus(provider.TxStatusSuccess).SetCurrentChain(targetChain.Name).Out(), nil +} + +func (r Bungee) Help() []string { + return []string{ + // BUNGEE_API_KEY env + "You can Set Custom Bungee API Key by setting the BUNGEE_API_KEY environment variable. The default value is " + apiKey, + "see https://www.bungee.exchange", + } +} + +func (r Bungee) Name() string { + return "bungee" +} + +func (r Bungee) Type() configs.ProviderType { + return configs.Bridge +} diff --git a/utils/provider/bridge/bungee/init.go b/utils/provider/bridge/bungee/init.go new file mode 100644 index 0000000..29bcdff --- /dev/null +++ b/utils/provider/bridge/bungee/init.go @@ -0,0 +1,10 @@ +package bungee + +import ( + "omni-balance/utils/configs" + "omni-balance/utils/provider" +) + +func init() { + provider.Register(configs.Bridge, New) +} diff --git a/utils/provider/bridge/bungee/utils.go b/utils/provider/bridge/bungee/utils.go new file mode 100644 index 0000000..fd5a7f1 --- /dev/null +++ b/utils/provider/bridge/bungee/utils.go @@ -0,0 +1,289 @@ +package bungee + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "math/big" + "net/url" + "omni-balance/utils" + "omni-balance/utils/chains" + "omni-balance/utils/constant" + "omni-balance/utils/error_types" + "omni-balance/utils/provider" + "strconv" + "strings" + "time" + + log "omni-balance/utils/logging" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/pkg/errors" + "github.com/shopspring/decimal" + "github.com/tidwall/gjson" +) + +var ( + pendingStatus = "pending" + completedStatus = "completed" +) + +type QuoteParams struct { + Sender common.Address `json:"sender"` + Receiver common.Address `json:"receiver"` + FromTokenAddress common.Address `json:"fromTokenAddress"` + ToTokenAddress common.Address `json:"toTokenAddress"` + AmountWei decimal.Decimal `json:"amount"` + FromTokenChainId int `json:"fromTokenChainId"` + ToTokenChainId int `json:"toTokenChainId"` +} + +func (r Bungee) Quote(ctx context.Context, args QuoteParams) (gjson.Result, error) { + params := &url.Values{} + params.Set("fromChainId", strconv.Itoa(args.FromTokenChainId)) + params.Set("fromTokenAddress", StandardizeZeroAddress(args.FromTokenAddress).Hex()) + params.Set("toChainId", strconv.Itoa(args.ToTokenChainId)) + params.Set("toTokenAddress", StandardizeZeroAddress(args.ToTokenAddress).Hex()) + params.Set("fromAmount", args.AmountWei.String()) + params.Set("userAddress", args.Sender.Hex()) + params.Set("sort", "output") + params.Set("recipient", args.Receiver.Hex()) + params.Set("singleTxOnly", "true") + u, _ := url.Parse("https://api.socket.tech/v2/quote") + u.RawQuery = params.Encode() + data, err := utils.RequestBinary(ctx, "GET", u.String(), nil, "API-KEY", apiKey) + if err != nil { + return gjson.Result{}, err + } + result := gjson.Parse(string(data)) + if !result.Get("success").Bool() { + return gjson.Result{}, errors.Errorf("quote error: %s", result.Get("message").String()) + } + return result, nil +} + +func (r Bungee) BuildTx(ctx context.Context, quote gjson.Result, sender, receiver common.Address) (*types.LegacyTx, error) { + var router = make(map[string]interface{}) + if err := json.Unmarshal([]byte(quote.Raw), &router); err != nil { + return nil, err + } + body := map[string]interface{}{ + "route": router, + } + reqData, err := json.Marshal(body) + if err != nil { + return nil, err + } + data, err := utils.RequestBinary(ctx, "POST", + "https://api.socket.tech/v2/build-tx", bytes.NewReader(reqData), "API-KEY", apiKey, "Content-Type", "application/json") + if err != nil { + return nil, err + } + result := gjson.Parse(string(data)) + if !result.Get("success").Bool() { + return nil, errors.Errorf("build tx error: %s", result.Get("message").String()) + } + txData := result.Get("result.txData").String() + if txData == "" { + return nil, errors.New("build tx error: txData is empty") + } + txTarget := result.Get("result.txTarget").String() + if txTarget == "" { + return nil, errors.New("build tx error: txTarget is empty") + } + value := big.NewInt(0) + value.SetString(txTarget, 16) + to := common.HexToAddress(txTarget) + return &types.LegacyTx{ + To: &to, + Value: value, + Data: common.Hex2Bytes(strings.TrimPrefix(txData, "0x")), + }, nil +} + +func (r Bungee) GetBestQuote(ctx context.Context, args provider.SwapParams) (tokenInName, tokenInChainName string, + tokenInAmount decimal.Decimal, quote gjson.Result, err error) { + if args.TargetToken == "" || args.TargetChain == "" { + return tokenInName, tokenInChainName, tokenInAmount, gjson.Result{}, errors.New("target token or target chain is empty") + } + + var ( + tokenOut = r.conf.GetTokenInfoOnChain(args.TargetToken, args.TargetChain) + msg = fmt.Sprintf("wallet %s rebalance %s on %s", args.Receiver, args.TargetToken, args.TargetChain) + ) + + getQuote := func(chainName, tokenName string) { + sourceToken := r.conf.GetTokenInfoOnChain(tokenName, chainName) + chain := r.conf.GetChainConfig(chainName) + tokenIn := r.conf.GetTokenInfoOnChain(sourceToken.Name, chainName) + if tokenIn.ContractAddress == "" { + log.Warnf("#%d %s %s tokenIn contract address is empty", args.OrderId, msg, tokenIn.Name) + return + } + client, err := chains.NewTryClient(ctx, chain.RpcEndpoints) + if err != nil { + log.Warnf("#%d %s %s get chain %s client error: %s", args.OrderId, msg, tokenIn.Name, chain.Name, err) + return + } + defer client.Close() + balance, err := chains.GetTokenBalance(ctx, client, tokenIn.ContractAddress, + args.Sender.GetAddress(true).Hex(), tokenIn.Decimals) + if err != nil { + log.Warnf("get %s on %s balance error: %s", tokenName, chainName, err) + return + } + if balance.LessThanOrEqual(decimal.Zero) { + return + } + + quoteData, err := r.Quote(ctx, QuoteParams{ + FromTokenAddress: common.HexToAddress(tokenOut.ContractAddress), + ToTokenAddress: common.HexToAddress(tokenIn.ContractAddress), + AmountWei: decimal.NewFromBigInt(chains.EthToWei(args.Amount, tokenOut.Decimals), 0), + FromTokenChainId: constant.GetChainId(args.TargetChain), + ToTokenChainId: chain.Id, + }) + if err != nil { + log.Debugf("#%d %s %s get quote error: %s", args.OrderId, msg, tokenIn.Name, err) + return + } + + if !quoteData.Get("success").Bool() { + log.Warnf("#%d %s %s get quote error: %s", args.OrderId, msg, tokenIn.Name, quoteData.Raw) + return + } + if len(quoteData.Get("result.routes").Array()) == 0 { + return + } + tokenAmount := quoteData.Get("result.routes.0.toAmount").String() + if tokenAmount == "" { + log.Warnf("#%d %s %s get quote error: destination token amount is empty", args.OrderId, msg, tokenIn.Name) + return + } + + tokenOutAmount := chains.WeiToEth(decimal.RequireFromString(tokenAmount).BigInt(), tokenIn.Decimals) + // 0.2% slippage + 0.2% fee + needTokenInAmount := tokenOutAmount.Add(tokenOutAmount.Mul(decimal.RequireFromString("0.004"))) + + if needTokenInAmount.GreaterThan(balance) { + log.Debugf("%s need %s on %s balance is greater than balance, need: %s, balance: %s", + args.Sender.GetAddress(true).Hex(), tokenName, chainName, needTokenInAmount.String(), balance.String()) + return + } + if tokenInAmount.Equal(decimal.Zero) { + tokenInAmount = needTokenInAmount + } + if tokenInAmount.GreaterThan(needTokenInAmount) { + return + } + + tokenInAmount = needTokenInAmount + tokenInName = sourceToken.Name + tokenInChainName = chainName + quote = quoteData + } + + if args.SourceChain != "" && args.SourceToken != "" { + getQuote(args.SourceChain, args.SourceToken) + if tokenInChainName == "" || tokenInName == "" || tokenInAmount.IsZero() { + return "", "", tokenInAmount, gjson.Result{}, error_types.ErrUnsupportedTokenAndChain + } + log.Debugf("#%d get best route for %s %s is use %s %s token from %s chain by specify source chain and token", args.OrderId, args.TargetChain, args.TargetToken, tokenInAmount, tokenInName, tokenInChainName) + return + } + + if args.SourceToken != "" && len(args.SourceChainNames) > 0 && args.SourceChain == "" { + for _, v := range args.SourceChainNames { + getQuote(v, args.SourceToken) + } + if tokenInChainName == "" || tokenInName == "" || tokenInAmount.IsZero() { + return "", "", tokenInAmount, gjson.Result{}, error_types.ErrUnsupportedTokenAndChain + } + log.Debugf("#%d best route for %s %s is use %s %s token from %s chain by specify source chains and token", args.OrderId, args.TargetChain, args.TargetToken, tokenInAmount, tokenInName, tokenInChainName) + return + } + + for _, sourceToken := range r.conf.SourceTokens { + if args.SourceToken != "" && sourceToken.Name != args.SourceToken { + continue + } + for _, v := range sourceToken.Chains { + if args.SourceChain != "" && v != args.SourceChain { + continue + } + getQuote(v, sourceToken.Name) + } + } + if tokenInChainName == "" || tokenInName == "" || tokenInAmount.IsZero() { + return "", "", tokenInAmount, gjson.Result{}, error_types.ErrUnsupportedTokenAndChain + } + log.Debugf("#%d best route for %s %s is use %s %s token from %s chain", args.OrderId, args.TargetChain, args.TargetToken, tokenInAmount, tokenInName, tokenInChainName) + return +} + +func (r Bungee) WaitForTx(ctx context.Context, fromChainId, toChainId int, hash common.Hash) error { + var ( + t = time.NewTicker(time.Second * 2) + count int64 + ) + defer t.Stop() + for count < 600 { + select { + case <-ctx.Done(): + log.Debugf("wait for tx %s canceled", hash) + return context.Canceled + case <-t.C: + log.Debugf("start check tx %s", hash) + result, err := r.Status(ctx, fromChainId, toChainId, hash) + if errors.Is(err, error_types.ErrNotFound) { + count++ + log.Debugf("tx %s not found, count: %d", hash, count) + continue + } + sourceTxStatus := result.Get("result.sourceTxStatus").String() + destinationTxStatus := result.Get("result.destinationTxStatus").String() + log.Infof("tx %s sourceTxStatus: %s; destinationTxStatus: %s", + hash, sourceTxStatus, destinationTxStatus) + if destinationTxStatus == "success" && sourceTxStatus == "success" { + return nil + } + if utils.InArrayFold(sourceTxStatus, []string{pendingStatus, "PENDING"}) { + continue + } + } + } + return errors.New("wait for tx timeout") +} + +func (r Bungee) Status(ctx context.Context, fromChainId, toChainId int, hash common.Hash) (gjson.Result, error) { + u, _ := url.Parse("https://api.socket.tech/v2/bridge-status") + query := u.Query() + query.Set("fromChainId", strconv.Itoa(fromChainId)) + query.Set("toChainId", strconv.Itoa(toChainId)) + query.Set("transactionHash", hash.Hex()) + u.RawQuery = query.Encode() + data, err := utils.RequestBinary(ctx, "Get", u.String(), nil, "API-KEY", apiKey) + if err != nil { + return gjson.Result{}, err + } + result := gjson.Parse(string(data)) + if !result.Get("success").Bool() { + return gjson.Result{}, errors.Errorf("status error: %s", result.Get("message").String()) + } + return result, nil +} + +// StandardizeZeroAddress standardizes the zero address. +// If the provided zeroAddress is equal to constant.ZeroAddress, it returns okxZeroAddress; otherwise, it returns the original address. +// Parameter: +// zeroAddress common.Address - The address to be standardized. +// Return: +// common.Address - The standardized okxZeroAddress +func StandardizeZeroAddress(address common.Address) common.Address { + if address.Cmp(constant.ZeroAddress) == 0 { + return ZeroAddress + } + return address +} diff --git a/utils/provider/bridge/bungee/vars.go b/utils/provider/bridge/bungee/vars.go new file mode 100644 index 0000000..6105b98 --- /dev/null +++ b/utils/provider/bridge/bungee/vars.go @@ -0,0 +1,183 @@ +package bungee + +import ( + "omni-balance/utils" + "omni-balance/utils/constant" + + "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + "github.com/shopspring/decimal" +) + +var ( + ZeroAddress = common.HexToAddress("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee") + // see https://docs.bungee.exchange/socket-api/support/chains + SupportedChain = map[string]struct{}{ + constant.Ethereum: {}, + constant.Bsc: {}, + constant.Fantom: {}, + constant.Avalanche: {}, + constant.Optimism: {}, + constant.Arbitrum: {}, + constant.Gnosis: {}, + constant.Polygon: {}, + constant.Aurora: {}, + constant.Zksync: {}, + constant.PolygonZkEvm: {}, + constant.Base: {}, + constant.Linea: {}, + constant.Scroll: {}, + constant.Blast: {}, + constant.Mantle: {}, + } + // see https://docs.bungee.exchange/socket-api/introduction#want-bungee-to-integrate-your-protocol + apiKey = utils.GetEnv("BUNGEE_API_KEY", "1b2fd225-062f-41aa-8c63-d1fef19945e7") +) + +var ( + ApproveTransactionAction = "ApproveTransaction" + SourceChainSendingAction = "sourceChainSending" + WaitForTxAction = "WaitForTx" +) + +type BaseResp struct { + ErrorMsg string `json:"error,omitempty"` + ErrorCode string `json:"error_code,omitempty"` +} + +type Quote struct { + BaseResp + FlowType string `json:"flowType"` + IsTransfer interface{} `json:"isTransfer"` + IsWrappedToken bool `json:"isWrappedToken"` + AllowanceTo string `json:"allowanceTo"` + BridgeFee struct { + Amount decimal.Decimal `json:"amount"` + Decimals int `json:"decimals"` + Symbol string `json:"symbol"` + Address string `json:"address"` + } `json:"bridgeFee"` + FuelTransfer interface{} `json:"fuelTransfer"` + FromTokenAddress common.Address `json:"fromTokenAddress"` + ToTokenAddress common.Address `json:"toTokenAddress"` + Source struct { + ChainId string `json:"chainId"` + ChainType string `json:"chainType"` + Asset struct { + Decimals int `json:"decimals"` + Symbol string `json:"symbol"` + Name string `json:"name"` + ChainId string `json:"chainId"` + Address string `json:"address"` + ResourceID string `json:"resourceID"` + IsMintable bool `json:"isMintable"` + IsWrappedAsset bool `json:"isWrappedAsset"` + IsReserveAsset bool `json:"isReserveAsset"` + TokenInstance struct { + Decimals int `json:"decimals"` + Symbol string `json:"symbol"` + Name string `json:"name"` + ChainId int `json:"chainId"` + Address string `json:"address"` + } `json:"tokenInstance"` + } `json:"asset"` + StableReserveAsset struct { + Decimals int `json:"decimals"` + Symbol string `json:"symbol"` + Name string `json:"name"` + ChainId string `json:"chainId"` + Address string `json:"address"` + ResourceID string `json:"resourceID"` + IsMintable *bool `json:"isMintable,omitempty"` + IsWrappedAsset *bool `json:"isWrappedAsset,omitempty"` + IsReserveAsset *bool `json:"isReserveAsset,omitempty"` + } `json:"stableReserveAsset"` + TokenAmount decimal.Decimal `json:"tokenAmount"` + StableReserveAmount string `json:"stableReserveAmount"` + Path []interface{} `json:"path"` + Flags []interface{} `json:"flags"` + PriceImpact string `json:"priceImpact"` + TokenPath string `json:"tokenPath"` + DataTx []string `json:"dataTx"` + } `json:"source"` + Destination struct { + ChainId string `json:"chainId"` + Asset struct { + Decimals int `json:"decimals"` + Symbol string `json:"symbol"` + Name string `json:"name"` + ChainId string `json:"chainId"` + Address string `json:"address"` + ResourceID string `json:"resourceID"` + IsMintable bool `json:"isMintable"` + IsWrappedAsset bool `json:"isWrappedAsset"` + IsReserveAsset bool `json:"isReserveAsset"` + TokenInstance struct { + Decimals int `json:"decimals"` + Symbol string `json:"symbol"` + Name string `json:"name"` + ChainId int `json:"chainId"` + Address string `json:"address"` + } `json:"tokenInstance"` + } `json:"asset"` + StableReserveAsset struct { + Decimals int `json:"decimals"` + Symbol string `json:"symbol"` + Name string `json:"name"` + ChainId string `json:"chainId"` + Address string `json:"address"` + ResourceID string `json:"resourceID"` + IsMintable bool `json:"isMintable"` + IsWrappedAsset bool `json:"isWrappedAsset"` + IsReserveAsset bool `json:"isReserveAsset"` + } `json:"stableReserveAsset"` + TokenAmount decimal.Decimal `json:"tokenAmount"` + } `json:"destination"` + PartnerId interface{} `json:"partnerId"` + SlippageTolerance interface{} `json:"slippageTolerance"` + EstimatedTime interface{} `json:"estimatedTime"` +} + +type Txn struct { + Quote + Txn struct { + From common.Address `json:"from"` + To common.Address `json:"to"` + Data string `json:"data"` + Value string `json:"value"` + GasPrice decimal.Decimal `json:"gasPrice"` + GasLimit decimal.Decimal `json:"gasLimit"` + } `json:"txn"` +} + +func (b BaseResp) Error() error { + if b.ErrorCode != "" { + return errors.New(b.ErrorMsg) + } + return nil +} +func (q Quote) Error() error { + if err := q.BaseResp.Error(); err != nil { + return err + } + if q.FlowType == "" { + return errors.New("flowType is empty") + } + if q.Destination.Asset.Symbol == "" { + return errors.New("destination.asset.symbol is empty") + } + return nil +} + +func Action2Int(action string) int { + switch action { + case ApproveTransactionAction: + return 1 + case SourceChainSendingAction: + return 2 + case WaitForTxAction: + return 3 + default: + return 0 + } +} diff --git a/utils/util.go b/utils/util.go index c94eba3..eb4740f 100644 --- a/utils/util.go +++ b/utils/util.go @@ -9,6 +9,7 @@ import ( "io" "math/big" "net/http" + "os" "reflect" "runtime/debug" "strings" @@ -293,3 +294,11 @@ func PadStringTo32Bytes(str string) []byte { } return buffer.Bytes() } + +func GetEnv(key string, defaultValue ...string) string { + value := os.Getenv(key) + if value == "" && len(defaultValue) > 0 { + return defaultValue[0] + } + return value +}