diff --git a/package.json b/package.json index 6bfb78e497..1b786ded87 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rubic-sdk", - "version": "4.54.3", + "version": "4.55.0", "description": "Simplify dApp creation", "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/src/features/cross-chain/calculation-manager/constants/cross-chain-providers.ts b/src/features/cross-chain/calculation-manager/constants/cross-chain-providers.ts index 84816c0551..b104337351 100644 --- a/src/features/cross-chain/calculation-manager/constants/cross-chain-providers.ts +++ b/src/features/cross-chain/calculation-manager/constants/cross-chain-providers.ts @@ -10,6 +10,7 @@ import { SquidrouterCrossChainProvider } from 'src/features/cross-chain/calculat import { SymbiosisCrossChainProvider } from 'src/features/cross-chain/calculation-manager/providers/symbiosis-provider/symbiosis-cross-chain-provider'; import { XyCrossChainProvider } from 'src/features/cross-chain/calculation-manager/providers/xy-provider/xy-cross-chain-provider'; +import { LayerZeroBridgeProvider } from '../providers/layerzero-bridge/layerzero-bridge-provider'; import { RangoCrossChainProvider } from '../providers/rango-provider/rango-cross-chain-provider'; import { StargateCrossChainProvider } from '../providers/stargate-provider/stargate-cross-chain-provider'; import { TaikoBridgeProvider } from '../providers/taiko-bridge/taiko-bridge-provider'; @@ -30,7 +31,8 @@ const nonProxyProviders = [ BridgersCrossChainProvider, ChangenowCrossChainProvider, ArbitrumRbcBridgeProvider, - TaikoBridgeProvider + TaikoBridgeProvider, + LayerZeroBridgeProvider // ScrollBridgeProvider ] as const; diff --git a/src/features/cross-chain/calculation-manager/models/cross-chain-trade-type.ts b/src/features/cross-chain/calculation-manager/models/cross-chain-trade-type.ts index 566320601e..6e188ab851 100644 --- a/src/features/cross-chain/calculation-manager/models/cross-chain-trade-type.ts +++ b/src/features/cross-chain/calculation-manager/models/cross-chain-trade-type.ts @@ -13,7 +13,8 @@ export const CROSS_CHAIN_TRADE_TYPE = { SCROLL_BRIDGE: 'scroll_bridge', TAIKO_BRIDGE: 'taiko_bridge', RANGO: 'rango', - PULSE_CHAIN_BRIDGE: 'pulsechain_bridge' + PULSE_CHAIN_BRIDGE: 'pulsechain_bridge', + LAYERZERO: 'layerzero' } as const; export type CrossChainTradeType = diff --git a/src/features/cross-chain/calculation-manager/providers/layerzero-bridge/constants/algb-token-addresses.ts b/src/features/cross-chain/calculation-manager/providers/layerzero-bridge/constants/algb-token-addresses.ts new file mode 100644 index 0000000000..13f65ab278 --- /dev/null +++ b/src/features/cross-chain/calculation-manager/providers/layerzero-bridge/constants/algb-token-addresses.ts @@ -0,0 +1,9 @@ +import { BLOCKCHAIN_NAME } from 'src/core/blockchain/models/blockchain-name'; + +import { LayerZeroBridgeSupportedBlockchain } from '../models/layerzero-bridge-supported-blockchains'; + +export const ALGB_TOKEN: Record = { + [BLOCKCHAIN_NAME.ARBITRUM]: '0x9f018bda8f6b507a0c9e6f290b2f7c49c2f8daf8', + [BLOCKCHAIN_NAME.BINANCE_SMART_CHAIN]: '0xe374116f490b461764e2438f98eab3fff383367b', + [BLOCKCHAIN_NAME.POLYGON]: '0x0169ec1f8f639b32eec6d923e24c2a2ff45b9dd6' +}; diff --git a/src/features/cross-chain/calculation-manager/providers/layerzero-bridge/constants/layerzero-bridge-address.ts b/src/features/cross-chain/calculation-manager/providers/layerzero-bridge/constants/layerzero-bridge-address.ts new file mode 100644 index 0000000000..cca3479c5f --- /dev/null +++ b/src/features/cross-chain/calculation-manager/providers/layerzero-bridge/constants/layerzero-bridge-address.ts @@ -0,0 +1,10 @@ +import { BLOCKCHAIN_NAME } from 'src/core/blockchain/models/blockchain-name'; + +import { LayerZeroBridgeSupportedBlockchain } from '../models/layerzero-bridge-supported-blockchains'; +import { ALGB_TOKEN } from './algb-token-addresses'; + +export const layerZeroProxyOFT: Record = { + [BLOCKCHAIN_NAME.ARBITRUM]: ALGB_TOKEN.ARBITRUM, + [BLOCKCHAIN_NAME.BINANCE_SMART_CHAIN]: ALGB_TOKEN.BSC, + [BLOCKCHAIN_NAME.POLYGON]: '0xDef87c507ef911Fd99c118c53171510Eb7967738' +}; diff --git a/src/features/cross-chain/calculation-manager/providers/layerzero-bridge/constants/layzerzero-chain-ids.ts b/src/features/cross-chain/calculation-manager/providers/layerzero-bridge/constants/layzerzero-chain-ids.ts new file mode 100644 index 0000000000..641219a81c --- /dev/null +++ b/src/features/cross-chain/calculation-manager/providers/layerzero-bridge/constants/layzerzero-chain-ids.ts @@ -0,0 +1,9 @@ +import { BLOCKCHAIN_NAME } from 'src/core/blockchain/models/blockchain-name'; + +import { LayerZeroBridgeSupportedBlockchain } from '../models/layerzero-bridge-supported-blockchains'; + +export const layerZeroChainIds: Record = { + [BLOCKCHAIN_NAME.ARBITRUM]: '110', + [BLOCKCHAIN_NAME.BINANCE_SMART_CHAIN]: '102', + [BLOCKCHAIN_NAME.POLYGON]: '109' +}; diff --git a/src/features/cross-chain/calculation-manager/providers/layerzero-bridge/layerzero-bridge-provider.ts b/src/features/cross-chain/calculation-manager/providers/layerzero-bridge/layerzero-bridge-provider.ts new file mode 100644 index 0000000000..22229c548b --- /dev/null +++ b/src/features/cross-chain/calculation-manager/providers/layerzero-bridge/layerzero-bridge-provider.ts @@ -0,0 +1,94 @@ +import { NotSupportedTokensError } from 'src/common/errors'; +import { PriceToken, PriceTokenAmount } from 'src/common/tokens'; +import { BlockchainName, EvmBlockchainName } from 'src/core/blockchain/models/blockchain-name'; +import { RequiredCrossChainOptions } from 'src/features/cross-chain/calculation-manager/models/cross-chain-options'; +import { CROSS_CHAIN_TRADE_TYPE } from 'src/features/cross-chain/calculation-manager/models/cross-chain-trade-type'; +import { CbridgeCrossChainSupportedBlockchain } from 'src/features/cross-chain/calculation-manager/providers/cbridge/constants/cbridge-supported-blockchains'; +import { CrossChainProvider } from 'src/features/cross-chain/calculation-manager/providers/common/cross-chain-provider'; +import { CalculationResult } from 'src/features/cross-chain/calculation-manager/providers/common/models/calculation-result'; +import { FeeInfo } from 'src/features/cross-chain/calculation-manager/providers/common/models/fee-info'; +import { RubicStep } from 'src/features/cross-chain/calculation-manager/providers/common/models/rubicStep'; + +import { LayerZeroBridgeTrade } from './layerzero-bridge-trade'; +import { + LayerZeroBridgeSupportedBlockchain, + layerZeroBridgeSupportedBlockchains +} from './models/layerzero-bridge-supported-blockchains'; + +export class LayerZeroBridgeProvider extends CrossChainProvider { + public readonly type = CROSS_CHAIN_TRADE_TYPE.LAYERZERO; + + public isSupportedBlockchain( + blockchain: BlockchainName + ): blockchain is LayerZeroBridgeSupportedBlockchain { + return layerZeroBridgeSupportedBlockchains.some( + supportedBlockchain => supportedBlockchain === blockchain + ); + } + + public async calculate( + fromToken: PriceTokenAmount, + toToken: PriceToken, + options: RequiredCrossChainOptions + ): Promise { + const fromBlockchain = fromToken.blockchain as LayerZeroBridgeSupportedBlockchain; + const toBlockchain = toToken.blockchain as LayerZeroBridgeSupportedBlockchain; + + if (!this.areSupportedBlockchains(fromBlockchain, toBlockchain)) { + return { + trade: null, + error: new NotSupportedTokensError(), + tradeType: this.type + }; + } + + try { + const to = new PriceTokenAmount({ + ...toToken.asStruct, + tokenAmount: fromToken.tokenAmount + }); + + const gasData = + options.gasCalculation === 'enabled' + ? await LayerZeroBridgeTrade.getGasData(fromToken, to, options) + : null; + + return { + trade: new LayerZeroBridgeTrade( + { + from: fromToken, + to, + gasData + }, + options.providerAddress, + await this.getRoutePath(fromToken, to) + ), + tradeType: this.type + }; + } catch (err) { + const rubicSdkError = CrossChainProvider.parseError(err); + + return { + trade: null, + error: rubicSdkError, + tradeType: this.type + }; + } + } + + protected async getFeeInfo( + _fromBlockchain: CbridgeCrossChainSupportedBlockchain, + _providerAddress: string, + _percentFeeToken: PriceTokenAmount, + _useProxy: boolean + ): Promise { + return {}; + } + + protected async getRoutePath( + fromToken: PriceTokenAmount, + toToken: PriceTokenAmount + ): Promise { + return [{ type: 'cross-chain', provider: this.type, path: [fromToken, toToken] }]; + } +} diff --git a/src/features/cross-chain/calculation-manager/providers/layerzero-bridge/layerzero-bridge-trade.ts b/src/features/cross-chain/calculation-manager/providers/layerzero-bridge/layerzero-bridge-trade.ts new file mode 100644 index 0000000000..2ae1ea9f67 --- /dev/null +++ b/src/features/cross-chain/calculation-manager/providers/layerzero-bridge/layerzero-bridge-trade.ts @@ -0,0 +1,252 @@ +import BigNumber from 'bignumber.js'; +import { solidityPack } from 'ethers/lib/utils'; +import { PriceTokenAmount } from 'src/common/tokens'; +import { BLOCKCHAIN_NAME, EvmBlockchainName } from 'src/core/blockchain/models/blockchain-name'; +import { EvmWeb3Pure } from 'src/core/blockchain/web3-pure/typed-web3-pure/evm-web3-pure/evm-web3-pure'; +import { Web3Pure } from 'src/core/blockchain/web3-pure/web3-pure'; +import { Injector } from 'src/core/injector/injector'; +import { ContractParams } from 'src/features/common/models/contract-params'; +import { SwapTransactionOptions } from 'src/features/common/models/swap-transaction-options'; +import { CROSS_CHAIN_TRADE_TYPE } from 'src/features/cross-chain/calculation-manager/models/cross-chain-trade-type'; +import { EvmCrossChainTrade } from 'src/features/cross-chain/calculation-manager/providers/common/emv-cross-chain-trade/evm-cross-chain-trade'; +import { GasData } from 'src/features/cross-chain/calculation-manager/providers/common/emv-cross-chain-trade/models/gas-data'; +import { BRIDGE_TYPE } from 'src/features/cross-chain/calculation-manager/providers/common/models/bridge-type'; +import { FeeInfo } from 'src/features/cross-chain/calculation-manager/providers/common/models/fee-info'; +import { RubicStep } from 'src/features/cross-chain/calculation-manager/providers/common/models/rubicStep'; +import { TradeInfo } from 'src/features/cross-chain/calculation-manager/providers/common/models/trade-info'; + +import { convertGasDataToBN } from '../../utils/convert-gas-price'; +import { ALGB_TOKEN } from './constants/algb-token-addresses'; +import { layerZeroProxyOFT } from './constants/layerzero-bridge-address'; +import { layerZeroChainIds } from './constants/layzerzero-chain-ids'; +import { LayerZeroBridgeSupportedBlockchain } from './models/layerzero-bridge-supported-blockchains'; +import { layerZeroOFTABI } from './models/layerzero-oft-abi'; + +export class LayerZeroBridgeTrade extends EvmCrossChainTrade { + /** @internal */ + public static async getGasData( + from: PriceTokenAmount, + to: PriceTokenAmount, + options: SwapTransactionOptions + ): Promise { + const fromBlockchain = from.blockchain as LayerZeroBridgeSupportedBlockchain; + const walletAddress = + Injector.web3PrivateService.getWeb3PrivateByBlockchain(fromBlockchain).address; + if (!walletAddress) { + return null; + } + + try { + const { contractAddress, contractAbi, methodName, methodArguments, value } = + await new LayerZeroBridgeTrade( + { + from, + to, + gasData: { + gasLimit: new BigNumber(0), + gasPrice: new BigNumber(0) + } + }, + EvmWeb3Pure.EMPTY_ADDRESS, + [] + ).getContractParams(options); + + const web3Public = Injector.web3PublicService.getWeb3Public(fromBlockchain); + const [gasLimit, gasDetails] = await Promise.all([ + web3Public.getEstimatedGas( + contractAbi, + contractAddress, + methodName, + methodArguments, + walletAddress, + value + ), + convertGasDataToBN(await Injector.gasPriceApi.getGasPrice(from.blockchain)) + ]); + + if (!gasLimit?.isFinite()) { + return null; + } + + const increasedGasLimit = Web3Pure.calculateGasMargin(gasLimit, 1.2); + return { + gasLimit: increasedGasLimit, + ...gasDetails + }; + } catch (_err) { + return null; + } + } + + public readonly onChainSubtype = { from: undefined, to: undefined }; + + public readonly type = CROSS_CHAIN_TRADE_TYPE.LAYERZERO; + + public readonly isAggregator = false; + + public readonly bridgeType = BRIDGE_TYPE.LAYERZERO; + + public readonly from: PriceTokenAmount; + + public readonly to: PriceTokenAmount; + + public readonly toTokenAmountMin: BigNumber; + + public readonly gasData: GasData | null; + + private get fromBlockchain(): LayerZeroBridgeSupportedBlockchain { + return this.from.blockchain as LayerZeroBridgeSupportedBlockchain; + } + + private get toBlockchain(): LayerZeroBridgeSupportedBlockchain { + return this.to.blockchain as LayerZeroBridgeSupportedBlockchain; + } + + protected get fromContractAddress(): string { + return layerZeroProxyOFT[this.fromBlockchain]; + } + + public readonly feeInfo: FeeInfo = {}; + + public readonly onChainTrade = null; + + protected get methodName(): string { + return 'sendFrom'; + } + + constructor( + crossChainTrade: { + from: PriceTokenAmount; + to: PriceTokenAmount; + gasData: GasData | null; + }, + providerAddress: string, + routePath: RubicStep[] + ) { + super(providerAddress, routePath); + + this.from = crossChainTrade.from; + this.to = crossChainTrade.to; + this.gasData = crossChainTrade.gasData; + this.toTokenAmountMin = crossChainTrade.to.tokenAmount; + } + + protected async swapDirect(options: SwapTransactionOptions = {}): Promise { + await this.checkTradeErrors(); + await this.checkAllowanceAndApprove(options); + + const { onConfirm, gasLimit, gasPrice, gasPriceOptions } = options; + let transactionHash: string; + const onTransactionHash = (hash: string) => { + if (onConfirm) { + onConfirm(hash); + } + transactionHash = hash; + }; + + // eslint-disable-next-line no-useless-catch + try { + const params = await this.getContractParams(options); + + const { data, to, value } = EvmWeb3Pure.encodeMethodCall( + params.contractAddress, + params.contractAbi, + params.methodName, + params.methodArguments, + params.value + ); + + const tx = await this.web3Private.trySendTransaction(to, { + data, + value, + gas: gasLimit, + gasPrice, + gasPriceOptions + }); + + onTransactionHash(tx.transactionHash); + + return transactionHash!; + } catch (err) { + throw err; + } + } + + public async getContractParams(options: SwapTransactionOptions): Promise { + const account = this.web3Private.address; + + const fee = await this.estimateSendFee(options); + + const methodArguments = [ + account, + layerZeroChainIds[this.toBlockchain], + options.receiverAddress || account, + this.from.stringWeiAmount, + options.receiverAddress || account, + '0x0000000000000000000000000000000000000000', + '0x' + ]; + + return { + contractAddress: + this.fromBlockchain === BLOCKCHAIN_NAME.POLYGON + ? layerZeroProxyOFT[BLOCKCHAIN_NAME.POLYGON] + : ALGB_TOKEN[this.fromBlockchain], + contractAbi: layerZeroOFTABI, + methodName: this.methodName, + methodArguments, + value: fee || '0x' + }; + } + + private async estimateSendFee(options: SwapTransactionOptions) { + const adapterParams = solidityPack( + ['uint16', 'uint256'], + [1, this.toBlockchain === BLOCKCHAIN_NAME.ARBITRUM ? 2_000_000 : 200_000] + ); + + const params = { + contractAddress: + this.fromBlockchain === BLOCKCHAIN_NAME.POLYGON + ? layerZeroProxyOFT[BLOCKCHAIN_NAME.POLYGON] + : ALGB_TOKEN[this.fromBlockchain], + contractAbi: layerZeroOFTABI, + methodName: 'estimateSendFee', + methodArguments: [ + layerZeroChainIds[this.toBlockchain], + options.receiverAddress || this.web3Private.address, + this.from.stringWeiAmount, + false, + adapterParams + ], + value: '0' + }; + + const gasFee = await this.fromWeb3Public.callContractMethod( + params.contractAddress, + params.contractAbi, + params.methodName, + params.methodArguments + ); + + return gasFee[0]; + } + + public getTradeAmountRatio(fromUsd: BigNumber): BigNumber { + return fromUsd.dividedBy(this.to.tokenAmount); + } + + public getUsdPrice(): BigNumber { + return this.from.price.multipliedBy(this.from.tokenAmount); + } + + public getTradeInfo(): TradeInfo { + return { + estimatedGas: this.estimatedGas, + feeInfo: this.feeInfo, + priceImpact: null, + slippage: 0, + routePath: this.routePath + }; + } +} diff --git a/src/features/cross-chain/calculation-manager/providers/layerzero-bridge/models/layerzero-bridge-supported-blockchains.ts b/src/features/cross-chain/calculation-manager/providers/layerzero-bridge/models/layerzero-bridge-supported-blockchains.ts new file mode 100644 index 0000000000..31a79024c3 --- /dev/null +++ b/src/features/cross-chain/calculation-manager/providers/layerzero-bridge/models/layerzero-bridge-supported-blockchains.ts @@ -0,0 +1,10 @@ +import { BLOCKCHAIN_NAME } from 'src/core/blockchain/models/blockchain-name'; + +export const layerZeroBridgeSupportedBlockchains = [ + BLOCKCHAIN_NAME.POLYGON, + BLOCKCHAIN_NAME.BINANCE_SMART_CHAIN, + BLOCKCHAIN_NAME.ARBITRUM +] as const; + +export type LayerZeroBridgeSupportedBlockchain = + (typeof layerZeroBridgeSupportedBlockchains)[number]; diff --git a/src/features/cross-chain/calculation-manager/providers/layerzero-bridge/models/layerzero-oft-abi.ts b/src/features/cross-chain/calculation-manager/providers/layerzero-bridge/models/layerzero-oft-abi.ts new file mode 100644 index 0000000000..7a947a70fa --- /dev/null +++ b/src/features/cross-chain/calculation-manager/providers/layerzero-bridge/models/layerzero-oft-abi.ts @@ -0,0 +1,91 @@ +import { AbiItem } from 'web3-utils'; + +export const layerZeroOFTABI: AbiItem[] = [ + { + inputs: [ + { + internalType: 'uint16', + name: '_dstChainId', + type: 'uint16' + }, + { + internalType: 'bytes', + name: '_toAddress', + type: 'bytes' + }, + { + internalType: 'uint256', + name: '_amount', + type: 'uint256' + }, + { + internalType: 'bool', + name: '_useZro', + type: 'bool' + }, + { + internalType: 'bytes', + name: '_adapterParams', + type: 'bytes' + } + ], + name: 'estimateSendFee', + outputs: [ + { + internalType: 'uint256', + name: 'nativeFee', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'zroFee', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'address', + name: '_from', + type: 'address' + }, + { + internalType: 'uint16', + name: '_dstChainId', + type: 'uint16' + }, + { + internalType: 'bytes', + name: '_toAddress', + type: 'bytes' + }, + { + internalType: 'uint256', + name: '_amount', + type: 'uint256' + }, + { + internalType: 'address payable', + name: '_refundAddress', + type: 'address' + }, + { + internalType: 'address', + name: '_zroPaymentAddress', + type: 'address' + }, + { + internalType: 'bytes', + name: '_adapterParams', + type: 'bytes' + } + ], + name: 'sendFrom', + outputs: [], + stateMutability: 'payable', + type: 'function' + } +]; diff --git a/src/features/cross-chain/status-manager/cross-chain-status-manager.ts b/src/features/cross-chain/status-manager/cross-chain-status-manager.ts index 8a3313f04d..c39cd2c773 100644 --- a/src/features/cross-chain/status-manager/cross-chain-status-manager.ts +++ b/src/features/cross-chain/status-manager/cross-chain-status-manager.ts @@ -84,13 +84,14 @@ export class CrossChainStatusManager { [CROSS_CHAIN_TRADE_TYPE.XY]: this.getXyDstSwapStatus, [CROSS_CHAIN_TRADE_TYPE.CELER_BRIDGE]: this.getCelerBridgeDstSwapStatus, [CROSS_CHAIN_TRADE_TYPE.CHANGENOW]: this.getChangenowDstSwapStatus, - [CROSS_CHAIN_TRADE_TYPE.STARGATE]: this.getStargateDstSwapStatus, + [CROSS_CHAIN_TRADE_TYPE.STARGATE]: this.getLayerZeroDstSwapStatus, [CROSS_CHAIN_TRADE_TYPE.ARBITRUM]: this.getArbitrumBridgeDstSwapStatus, [CROSS_CHAIN_TRADE_TYPE.SQUIDROUTER]: this.getSquidrouterDstSwapStatus, [CROSS_CHAIN_TRADE_TYPE.SCROLL_BRIDGE]: this.getScrollBridgeDstSwapStatus, [CROSS_CHAIN_TRADE_TYPE.TAIKO_BRIDGE]: this.getTaikoBridgeDstSwapStatus, [CROSS_CHAIN_TRADE_TYPE.RANGO]: this.getRangoDstSwapStatus, - [CROSS_CHAIN_TRADE_TYPE.PULSE_CHAIN_BRIDGE]: this.getPulseChainDstSwapStatus + [CROSS_CHAIN_TRADE_TYPE.PULSE_CHAIN_BRIDGE]: this.getPulseChainDstSwapStatus, + [CROSS_CHAIN_TRADE_TYPE.LAYERZERO]: this.getLayerZeroDstSwapStatus }; /** @@ -167,7 +168,7 @@ export class CrossChainStatusManager { * @param data Trade data. * @returns Cross-chain transaction status and hash. */ - private async getStargateDstSwapStatus(data: CrossChainTradeData): Promise { + private async getLayerZeroDstSwapStatus(data: CrossChainTradeData): Promise { const lzPackage = await import('@layerzerolabs/scan-client'); const client = lzPackage.createClient('mainnet'); const scanResponse = await client.getMessagesBySrcTxHash(data.srcTxHash);