import di from 'a-di'; import { BlockchainExplorerFactory } from '@dequanto/explorer/BlockchainExplorerFactory'; import { EoAccount } from "@dequanto/models/TAccount"; import { Web3ClientFactory } from '@dequanto/clients/Web3ClientFactory'; import { IToken } from '@dequanto/models/IToken'; import { TPlatform } from '@dequanto/models/TPlatform'; import { TokenService } from '@dequanto/tokens/TokenService'; import { TokensServiceFactory } from '@dequanto/tokens/TokensServiceFactory'; import { $bigint } from '@dequanto/utils/$bigint'; import { $date } from '@dequanto/utils/$date'; import { networks } from '@hop-protocol/core/networks/mainnet'; import { Hop, Chain } from '@hop-protocol/sdk'; import { class_Dfr } from 'atma-utils'; import { HopAddresses } from './HopAddresses'; import { $fn } from '@dequanto/utils/$fn'; import { IBridge } from '../models/IBridge'; import { TAddress } from '@dequanto/models/TAddress'; import { TokensService } from '@dequanto/tokens/TokensService'; import { PlatformFactory } from '@dequanto/chains/PlatformFactory'; import { $require } from '@dequanto/utils/$require'; import { ITxBuilderOptions } from '@dequanto/txs/ITxBuilderOptions'; import { ITxWriterOptions } from '@dequanto/txs/TxWriter'; import { $address } from '@dequanto/utils/$address'; import { $promise } from '@dequanto/utils/$promise'; import { TEth } from '@dequanto/models/TEth'; /** HOP protocol was partially implemented, but will take some time to implement bridges packages */ type THopAddresses = typeof HopAddresses; export class HopBridge implements IBridge { name = 'hop' // tested and used chains private SUPPORTED_CHAINS: TPlatform[] = [ 'polygon', 'xdai' ] async withdraw (account: EoAccount, platform: TPlatform, symbol: string, params?: { config?: ITxBuilderOptions amount?: number }) { $require.oneOf(platform, this.SUPPORTED_CHAINS, 'Supporting only tested chains'); $require.match(/^h/i, symbol, 'We can withdraw hop wrapped tokens, like "hDAI"'); symbol = symbol.substring(1).toUpperCase(); let addresses = HopAddresses.bridges[symbol]?.[platform] as THopAddresses['bridges']['DAI']['polygon']; if (addresses == null) { throw new Error(`No bridge addresses for symbol ${symbol} and platform ${platform}`); } let swapAddress = addresses.l2SaddleSwap; $require.Address(swapAddress, `Swap Address undefined for ${symbol} and ${platform}`); let bridgeTokenAddress = addresses.l2HopBridgeToken; $require.Address(bridgeTokenAddress, `Bridge Token undefined for ${symbol} and ${platform}`); let chain = await di.resolve(PlatformFactory).get(platform); let swapContract = new Dai_l2SaddleSwapContract( swapAddress, chain.client, chain.explorer, ); let tokens = await Promise.all([ swapContract.getToken(0), swapContract.getToken(1), ]); let hTokenAddr = tokens.find(x => $address.eq(x, bridgeTokenAddress)); $require.notNull(hTokenAddr, `Bridge Token (${bridgeTokenAddress}) not found in ${tokens}`); let hTokenIndex = tokens.indexOf(hTokenAddr); let hToken = { decimals: 18, address: hTokenAddr, platform, symbol: 'hDAI' }; let hTokenContract = await chain.tokens.erc20(hToken); let amount = await hTokenContract.balanceOf(account.address); console.log(amount); let txApprove = await chain.token.ensureApproved(account, hToken, swapContract.address, amount); if (txApprove) { await txApprove.onCompleted; } let tx = await swapContract .$config(params?.config ?? {}) .swap( account, hTokenIndex, hTokenIndex === 0 ? 1 : 0, amount, $bigint.multWithFloat(amount, .97), BigInt($date.tool().add('24h').toUnixTimestamp()) ); return tx; } async canTransfer (account: TAddress, amount: null | number | bigint, symbol: string, fromPlatform: TPlatform, toPlatform: TPlatform): Promise<{error: Error | null}> { const client = Web3ClientFactory.get(fromPlatform); const tokenService = di.resolve(TokenService, client); const tokensService = TokensServiceFactory.get(fromPlatform); const TokenBridges = this.getTokenBridges(symbol); this.checkPlatforms(fromPlatform, toPlatform, symbol, TokenBridges); let token = await this.getToken(tokensService, symbol, fromPlatform); let balance = await tokenService.balanceOf(account, token); if (balance === 0n) { return { error: new Error(`Balance is empty to transfer`) }; } if (amount != null) { let amountWei = this.getAmount(amount, token); if (balance < amountWei) { return { error: new Error(`Invalid balance to transfer`) }; } } return { error: null } } async transfer (account: EoAccount, amount: number | bigint, symbol: string, fromPlatform: TPlatform, toPlatform: TPlatform) { const chainFrom = await di.resolve(PlatformFactory).get(fromPlatform); // const client = Web3ClientFactory.get(fromPlatform); // const tokenService = di.resolve(TokenService, client); // const tokensService = TokensServiceFactory.get(fromPlatform); let token = await this.getToken(chainFrom.tokens, symbol, fromPlatform); let amountWei = this.getAmount(amount, token); let TokenBridges = this.getTokenBridges(symbol); this.checkPlatforms(fromPlatform, toPlatform, symbol, TokenBridges); const url = await chainFrom.client.getNodeURL({ ws: false }); const provider = new providers.JsonRpcProvider(url, Utils.getChainId(fromPlatform)); // WORKAROUND: make hop sdk to use our RPC. networks[fromPlatform].publicRpcUrl = url; const signer = new Wallet(account.key, provider); const hop = new Hop('mainnet', signer); const bridge = hop.bridge(symbol); let BridgeAddresses = this.getBridgeAddresses(fromPlatform, symbol, TokenBridges); const l2AmmWrapperAddr = BridgeAddresses.l2AmmWrapper; const l2BridgeAddr = BridgeAddresses.l2Bridge; let withRemainder = amountWei < 0; let balance: bigint = null; let remainder: bigint = null; if (withRemainder) { remainder = amountWei * -1n; balance = await chainFrom.token.balanceOf(account.address, token); amountWei = balance + amountWei; let gasPrice = await chainFrom.client.getGasPrice(); let gasLimit = 300_000n; amountWei -= gasPrice.price * gasLimit; console.log(`Estimated GAS Price: ${ $bigint.toGweiFromWei(gasPrice.price) }GWEI; Total: ${ $bigint.toGweiFromWei(gasPrice.price * gasLimit) }GWEI`); if (amountWei < 0) { throw new Error(`Not enough amount to be left on chain. Balance ${balance}`); } } const isNative = chainFrom.tokens.isNative(token); if (isNative === false) { // ERC20, check approval let txApprove = await chainFrom.token.ensureApproved(account, token, l2AmmWrapperAddr, amountWei); if (txApprove) { await txApprove.onCompleted; } } // console.log( // 'Hop.transfer', // symbol, l2AmmWrapperAddr, // chainFrom.client.platform, // fromPlatform, // toPlatform, // url // ); let depositContract = new Dai_l2AmmWrapperContract( l2AmmWrapperAddr, chainFrom.client, BlockchainExplorerFactory.get(fromPlatform) ); let bridgeContract = new Dai_l2BridgeContract( l2BridgeAddr, chainFrom.client, BlockchainExplorerFactory.get(fromPlatform) ); let deadline = bridge.defaultDeadlineSeconds; // BigInt($date.tool().add(`${bridge.defaultDeadlineSeconds}s`).toUnixTimestamp()); let sendData = await bridge.getSendData( amountWei.toString(), Utils.getChain(fromPlatform), Utils.getChain(toPlatform), ); let params = { bonderFee: BigInt(sendData.adjustedBonderFee.toString()), txFee: BigInt(sendData.adjustedDestinationTxFee.toString()), totalFee: BigInt(sendData.totalFee.toString()), estimatedReceived: BigInt(sendData.estimatedReceived.toString()), amountOut: BigInt(sendData.amountOut.toString()) }; // decrease awaitence params.amountOut = $bigint.multWithFloat(params.amountOut, .98); let txWriter = await depositContract .$config({ gasLimit: 300_000, gasLimitRatio: 1, }) .swapAndSend( { ...account, value: isNative ? amountWei : 0n }, BigInt(Utils.getChainId(toPlatform)), account.address, amountWei, params.totalFee, params.amountOut - $bigint.toWei(.15, token.decimals), BigInt(deadline), params.estimatedReceived - $bigint.toWei(.15, token.decimals), BigInt(deadline) ); let result = { txWriter: txWriter, txReceipt: new class_Dfr(), txTransferId: new class_Dfr() }; (async function () { try { let receipt = await txWriter.onCompleted; result.txReceipt.resolve(receipt); let [ event ] = bridgeContract.extractLogsTransferSent(receipt); if (event == null) { throw new Error(`TransferSent Event was not parsed`); } result.txTransferId.resolve(event.transferId as string); } catch (error) { console.log(`HopBridge transfer ${error.message}`); result.txTransferId.reject(error); } }()); return result; } async waitForTransfer (transferId: string, symbol: string, toPlatform: TPlatform, toAccount: TAddress): Promise { let r = await $promise.waitForObject(async () => { let val = await this.status(transferId, symbol, toPlatform); console.log(`${ $date.format(new Date(), 'HH:mm') } Checked for bridge transfer completion: ${val}`); if (val) { return [ null, {} ] } return null; }, { intervalMs: 20000, timeoutMs: 5 * 60 * 60 * 1000, timeoutMessage: `Waiting the Transfer to be Completed timeouted` }) } async status (transferId: string, symbol: string, toPlatform: TPlatform): Promise { symbol = symbol.toUpperCase(); if (symbol === 'XDAI') { symbol = 'DAI'; } let TokenBridges = HopAddresses.bridges[ symbol ]; if (TokenBridges == null) { throw new Error(`Token ${symbol} is not supported by the HOP Protocol`); } let bridgeToPlatform = getBridgePlatform(toPlatform); if (bridgeToPlatform in TokenBridges === false) { throw new Error(`Unsupported chain ("${toPlatform}") to transfer ${symbol} token to`); } let BridgeAddresses = TokenBridges[bridgeToPlatform]; if (BridgeAddresses == null) { throw new Error(`No BridgeAddresses for ${toPlatform} and token ${symbol}`); } const l2BridgeAddr = BridgeAddresses.l2Bridge; const client = Web3ClientFactory.get(toPlatform); let bridgeContract = new Dai_l2BridgeContract( l2BridgeAddr, client, BlockchainExplorerFactory.get(toPlatform) ); return bridgeContract.isTransferIdSpent(transferId); } async transferWithSDK (account: EoAccount, amount: number | bigint, token: IToken, fromPlatform: TPlatform, toPlatform: TPlatform) { if (typeof amount === 'number') { amount = $bigint.toWei(amount, token.decimals); } const client = Web3ClientFactory.get(fromPlatform); const url = await client.getNodeURL({ ws: false }); const provider = new providers.JsonRpcProvider(url); const signer = new Wallet(account.key, provider); const hop = new Hop('mainnet', signer); const bridge = hop.bridge(token.symbol); const tx = await bridge.approveAndSend( amount.toString(), Utils.getChain(fromPlatform), Utils.getChain(toPlatform), ); console.log(tx); return tx; } private checkPlatforms (fromPlatform: TPlatform, toPlatform: TPlatform, symbol: string, TokenBridges) { if (this.SUPPORTED_CHAINS.includes(fromPlatform) === false) { throw new Error(`From ${fromPlatform} platform is not tested yet`); } if (this.SUPPORTED_CHAINS.includes(toPlatform) === false) { throw new Error(`To ${toPlatform} platform is not tested yet`); } /** throws if invalid */ this.getBridgePlatform(fromPlatform, symbol, TokenBridges); /** throws if invalid */ this.getBridgePlatform(toPlatform, symbol, TokenBridges); /** throws if invalid */ this.getBridgeAddresses(fromPlatform, symbol, TokenBridges); } private getAmount (amount: number | bigint, token: IToken): bigint { if (typeof amount === 'number') { return $bigint.toWei(amount, token.decimals); } return amount; } private async getToken (tokensService: TokensService, symbol: string, fromPlatform: TPlatform): Promise { if (fromPlatform === 'xdai') { if (symbol === 'DAI') { symbol = 'XDAI'; } } let token = await tokensService.getKnownToken(symbol); if (token.platform !== fromPlatform) { throw new Error(`Token ${symbol} has wrong chain: ${token.platform}. Transfering from ${fromPlatform}`); } return token; } private getTokenBridges (symbol: string) { let TokenBridges = HopAddresses.bridges[ symbol ] as THopAddresses['bridges']['USDC']; if (TokenBridges == null) { throw new Error(`Token ${symbol} is not supported by the HOP Protocol`); } return TokenBridges; } private getBridgePlatform (platform: TPlatform, symbol, TokenBridges): string { let bridgePlatform = getBridgePlatform(platform); if (bridgePlatform in TokenBridges === false) { throw new Error(`Unsupported chain ("${platform}") to transfer ${symbol} token from`); } return bridgePlatform; } private getBridgeAddresses (platform: TPlatform, symbol: string, TokenBridges) { let bridgePlatform = this.getBridgePlatform(platform, symbol, TokenBridges); let BridgeAddresses = TokenBridges[bridgePlatform]; if (BridgeAddresses == null) { throw new Error(`No BridgeAddresses for ${platform} and token ${symbol}`); } return BridgeAddresses; } } namespace Utils { export function getChain (platform: TPlatform) { switch (platform) { case 'eth': return Chain.Ethereum; case 'bsc': break; case 'polygon': return Chain.Polygon; case 'xdai': return Chain.xDai; case 'arbitrum': return Chain.Arbitrum; } throw new Error(`Not supported "${platform}" chain`); } export function getChainId (platform: TPlatform) { switch (platform) { case 'eth': return 1; case 'bsc': return 56; case 'polygon': return 137; case 'xdai': return 100; case 'arbitrum': return 42161; } throw new Error(`Not supported "${platform}" chain`); } } function getBridgePlatform (platform: TPlatform): string { switch (platform) { case 'eth': return 'ethereum'; default: return platform; } }