0xweb
Version:
Contract package manager and other web3 tools
438 lines (369 loc) • 16.6 kB
text/typescript
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<TEth.TxReceipt>(),
txTransferId: new class_Dfr<string>()
};
(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<void> {
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<boolean> {
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<IToken> {
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;
}
}