|
|
@@ -1,9 +1,40 @@
|
|
|
-import { Inject, Injectable, Logger } from '@nestjs/common'
|
|
|
+import { Inject, Injectable, InternalServerErrorException, Logger } from '@nestjs/common'
|
|
|
import { ethers } from 'ethers'
|
|
|
import { Wallet, Provider, utils } from 'zksync-web3'
|
|
|
import web3Config from './web3.config'
|
|
|
import { ConfigType } from '@nestjs/config'
|
|
|
import { AccountsService } from 'src/accounts/accounts.service'
|
|
|
+import erc20 from './erc20.json'
|
|
|
+import {
|
|
|
+ ChainId,
|
|
|
+ initialChainTable,
|
|
|
+ getGasToken,
|
|
|
+ point2PriceDecimal,
|
|
|
+ priceDecimal2Point,
|
|
|
+ pointDeltaRoundingDown,
|
|
|
+ pointDeltaRoundingUp,
|
|
|
+ amount2Decimal,
|
|
|
+ PriceRoundingType,
|
|
|
+ fetchToken,
|
|
|
+ getErc20TokenContract
|
|
|
+} from 'iziswap-sdk/lib/base'
|
|
|
+import Web3 from 'web3'
|
|
|
+import { BigNumber } from 'bignumber.js'
|
|
|
+import {} from 'iziswap-sdk/lib/quoter'
|
|
|
+import {} from 'iziswap-sdk/lib/swap'
|
|
|
+import {
|
|
|
+ calciZiLiquidityAmountDesired,
|
|
|
+ getMintCall,
|
|
|
+ getLiquidityManagerContract,
|
|
|
+ getPoolAddress,
|
|
|
+ fetchLiquiditiesOfAccount,
|
|
|
+ getDecLiquidityCall,
|
|
|
+ DecLiquidityParam,
|
|
|
+ getWithdrawLiquidityValue
|
|
|
+} from 'iziswap-sdk/lib/liquidityManager'
|
|
|
+import { getPoolContract, getPoolState, getPointDelta } from 'iziswap-sdk/lib/pool'
|
|
|
+import { getQuoterContract, quoterSwapChainWithExactOutput } from 'iziswap-sdk/lib/quoter'
|
|
|
+import { getSwapContract, getSwapChainWithExactOutputCall } from 'iziswap-sdk/lib/swap'
|
|
|
|
|
|
@Injectable()
|
|
|
export class Web3Service {
|
|
|
@@ -13,7 +44,7 @@ export class Web3Service {
|
|
|
private readonly accountService: AccountsService
|
|
|
) {}
|
|
|
|
|
|
- async zkDeposite(accountId, amount) {
|
|
|
+ async zkDeposit(accountId, amount) {
|
|
|
const account = await this.accountService.findById(accountId)
|
|
|
const provider = new Provider(this.web3Configuration.zksyncRpcUrl)
|
|
|
const ethProvider = new ethers.providers.InfuraProvider(
|
|
|
@@ -44,5 +75,296 @@ export class Web3Service {
|
|
|
// Logger.log('finalizedEthBalance', ethers.utils.formatEther(finalizedEthBalance))
|
|
|
}
|
|
|
|
|
|
- async zkWidthdraw() {}
|
|
|
+ async zkWidthdraw(accountId, amount) {
|
|
|
+ const account = await this.accountService.findById(accountId)
|
|
|
+ const provider = new Provider(this.web3Configuration.zksyncRpcUrl)
|
|
|
+ const ethProvider = new ethers.providers.InfuraProvider(
|
|
|
+ this.web3Configuration.ethereumNetwork,
|
|
|
+ this.web3Configuration.infuraApiKey
|
|
|
+ )
|
|
|
+ const wallet = new Wallet(account.privateKey, provider, ethProvider)
|
|
|
+ const withdrawL2 = await wallet.withdraw({
|
|
|
+ token: utils.ETH_ADDRESS,
|
|
|
+ amount: ethers.utils.parseEther(amount)
|
|
|
+ })
|
|
|
+ Logger.log(`Widthdraw sent. ${this.web3Configuration.zksyncExplorer}/tx/${withdrawL2.hash}`)
|
|
|
+ // // Await processing of the deposit on L1
|
|
|
+ // const ethereumTxReceipt = await deposit.waitL1Commit()
|
|
|
+
|
|
|
+ // Logger.log('L1 deposit confirmed! Waiting for zkSync...')
|
|
|
+ // // Await processing the deposit on zkSync
|
|
|
+ // const depositReceipt = await deposit.wait()
|
|
|
+ // Logger.log('zkSync deposit committed.')
|
|
|
+
|
|
|
+ // Logger.log('Checking zkSync account balance')
|
|
|
+ // // Retrieving the current (committed) zkSync ETH balance of an account
|
|
|
+ // const committedEthBalance = await wallet.getBalance()
|
|
|
+ // Logger.log('committedEthBalance', ethers.utils.formatEther(committedEthBalance))
|
|
|
+
|
|
|
+ // // Retrieving the ETH balance of an account in the last finalized zkSync block.
|
|
|
+ // const finalizedEthBalance = await wallet.getBalance(utils.ETH_ADDRESS, 'finalized')
|
|
|
+ // Logger.log('finalizedEthBalance', ethers.utils.formatEther(finalizedEthBalance))
|
|
|
+ }
|
|
|
+
|
|
|
+ async addLiquidity(accountId, amount) {
|
|
|
+ const chainId = this.web3Configuration.ethereumNetwork == 'goerli' ? ChainId.ZkSyncAlphaTest : ChainId.ZkSyncEra
|
|
|
+ const account = await this.accountService.findById(accountId)
|
|
|
+ const provider = new Provider(this.web3Configuration.zksyncRpcUrl)
|
|
|
+ const zkWallet = new Wallet(account.privateKey).connect(provider)
|
|
|
+ const chain = initialChainTable[chainId]
|
|
|
+ const web3 = new Web3(new Web3.providers.HttpProvider(this.web3Configuration.zksyncRpcUrl))
|
|
|
+ console.log('address: ', zkWallet.address)
|
|
|
+
|
|
|
+ const liquidityManagerContract = getLiquidityManagerContract(
|
|
|
+ this.web3Configuration.liquidityManagerAddress,
|
|
|
+ web3 as any
|
|
|
+ )
|
|
|
+ console.log('liquidity manager address: ', this.web3Configuration.liquidityManagerAddress)
|
|
|
+
|
|
|
+ const tokenA = getGasToken(chainId)
|
|
|
+ console.log('tokenA: ', tokenA)
|
|
|
+ const tokenB = await fetchToken(this.web3Configuration.tokenAddress, chain, web3 as any)
|
|
|
+ console.log('tokenB: ', tokenB)
|
|
|
+
|
|
|
+ await this.approve(tokenB.address, this.web3Configuration.liquidityManagerAddress, zkWallet)
|
|
|
+
|
|
|
+ const fee = 2000 // 2000 means 0.2%
|
|
|
+ const poolAddress = await getPoolAddress(liquidityManagerContract, tokenA, tokenB, fee)
|
|
|
+ console.log('pool address: ', poolAddress)
|
|
|
+
|
|
|
+ const poolContract = getPoolContract(poolAddress, web3 as any)
|
|
|
+ const state = await getPoolState(poolContract)
|
|
|
+ const currentPrice = point2PriceDecimal(tokenA, tokenB, state.currentPoint)
|
|
|
+ Logger.log(`current price: 1${tokenA.symbol}=${currentPrice}${tokenB.symbol}`, 'addLiquidity')
|
|
|
+
|
|
|
+ const point1 = priceDecimal2Point(tokenA, tokenB, currentPrice * 0.98, PriceRoundingType.PRICE_ROUNDING_NEAREST)
|
|
|
+ const point2 = priceDecimal2Point(tokenA, tokenB, currentPrice * 1.02, PriceRoundingType.PRICE_ROUNDING_NEAREST)
|
|
|
+ console.log('point range', point1, point2)
|
|
|
+ let leftPoint = Math.min(point1, point2)
|
|
|
+ let rightPoint = Math.max(point1, point2)
|
|
|
+ const pointDelta = await getPointDelta(poolContract)
|
|
|
+ console.log('point delta: ', pointDelta)
|
|
|
+ leftPoint = pointDeltaRoundingDown(leftPoint, pointDelta)
|
|
|
+ rightPoint = pointDeltaRoundingUp(rightPoint, pointDelta)
|
|
|
+
|
|
|
+ const maxTestA = new BigNumber(amount).times(10 ** tokenA.decimal)
|
|
|
+ const maxTestB = calciZiLiquidityAmountDesired(
|
|
|
+ leftPoint,
|
|
|
+ rightPoint,
|
|
|
+ state.currentPoint,
|
|
|
+ maxTestA,
|
|
|
+ true,
|
|
|
+ tokenA,
|
|
|
+ tokenB
|
|
|
+ )
|
|
|
+ const mintParams = {
|
|
|
+ tokenA: tokenA,
|
|
|
+ tokenB: tokenB,
|
|
|
+ fee,
|
|
|
+ leftPoint,
|
|
|
+ rightPoint,
|
|
|
+ maxAmountA: maxTestA.toFixed(0),
|
|
|
+ maxAmountB: maxTestB.toFixed(0),
|
|
|
+ minAmountA: maxTestA.times(0.985).toFixed(0),
|
|
|
+ minAmountB: maxTestB.times(0.985).toFixed(0)
|
|
|
+ }
|
|
|
+ console.log(JSON.stringify(mintParams, null, 4))
|
|
|
+ console.log(amount2Decimal(new BigNumber(maxTestB), tokenB))
|
|
|
+
|
|
|
+ const gasPrice = await web3.eth.getGasPrice()
|
|
|
+ const { mintCalling, options } = getMintCall(
|
|
|
+ liquidityManagerContract,
|
|
|
+ zkWallet.address,
|
|
|
+ chain,
|
|
|
+ mintParams,
|
|
|
+ gasPrice.toString()
|
|
|
+ )
|
|
|
+ let calling = mintCalling
|
|
|
+ if (calling instanceof Array) {
|
|
|
+ calling = liquidityManagerContract.methods.multicall(mintCalling)
|
|
|
+ }
|
|
|
+ console.log({ ...options, from: zkWallet.address })
|
|
|
+ const gasLimit = await calling.estimateGas({ from: zkWallet.address })
|
|
|
+ console.log('gas limit: ', gasLimit)
|
|
|
+
|
|
|
+ // sign transaction
|
|
|
+ const tx = await zkWallet.sendTransaction({
|
|
|
+ from: options.from,
|
|
|
+ value: Web3.utils.numberToHex(options.value),
|
|
|
+ to: this.web3Configuration.liquidityManagerAddress,
|
|
|
+ data: calling.encodeABI(),
|
|
|
+ gasLimit
|
|
|
+ })
|
|
|
+ console.log('tx: ', tx)
|
|
|
+ }
|
|
|
+
|
|
|
+ async removeLiquidity(accountId) {
|
|
|
+ const chainId = this.web3Configuration.ethereumNetwork == 'goerli' ? ChainId.ZkSyncAlphaTest : ChainId.ZkSyncEra
|
|
|
+ const account = await this.accountService.findById(accountId)
|
|
|
+ const provider = new Provider(this.web3Configuration.zksyncRpcUrl)
|
|
|
+ const wallet = new Wallet(account.privateKey).connect(provider)
|
|
|
+ const chain = initialChainTable[chainId]
|
|
|
+ const web3 = new Web3(new Web3.providers.HttpProvider(this.web3Configuration.zksyncRpcUrl))
|
|
|
+
|
|
|
+ const liquidityManagerContract = getLiquidityManagerContract(
|
|
|
+ this.web3Configuration.liquidityManagerAddress,
|
|
|
+ web3 as any
|
|
|
+ )
|
|
|
+ Logger.log('liquidity manager address: ', this.web3Configuration.liquidityManagerAddress)
|
|
|
+
|
|
|
+ const tokenA = getGasToken(chainId)
|
|
|
+ const tokenB = await fetchToken(this.web3Configuration.tokenAddress, chain, web3 as any)
|
|
|
+
|
|
|
+ Logger.log(`tokenA: ${tokenA.symbol} tokenB: ${tokenB.symbol}`, 'FetchLiquidities')
|
|
|
+ const liquidities = await fetchLiquiditiesOfAccount(
|
|
|
+ chain,
|
|
|
+ web3 as any,
|
|
|
+ liquidityManagerContract,
|
|
|
+ account.address,
|
|
|
+ [tokenA, tokenB]
|
|
|
+ )
|
|
|
+ Logger.log(`${liquidities.length} liquidities fetched`, 'FetchLiquidities')
|
|
|
+ if (liquidities.length <= 0) {
|
|
|
+ throw new InternalServerErrorException('No liquidity found')
|
|
|
+ }
|
|
|
+ const liquidity0 = liquidities.filter((i) => Number(i.liquidity) > 0)[0]
|
|
|
+ if (!liquidity0) {
|
|
|
+ throw new InternalServerErrorException('all liquidities are removed')
|
|
|
+ }
|
|
|
+
|
|
|
+ const decRate = 1
|
|
|
+ const originLiquidity = new BigNumber(liquidity0.liquidity)
|
|
|
+ const decLiquidity = originLiquidity.times(decRate)
|
|
|
+
|
|
|
+ const { amountX, amountY } = getWithdrawLiquidityValue(liquidity0, liquidity0.state, decLiquidity)
|
|
|
+ // here the slippery is 1.5%
|
|
|
+ const minAmountX = amountX.times(0.985).toFixed(0)
|
|
|
+ const minAmountY = amountY.times(0.985).toFixed(0)
|
|
|
+
|
|
|
+ const gasPrice = await web3.eth.getGasPrice()
|
|
|
+ const { decLiquidityCalling, options } = getDecLiquidityCall(
|
|
|
+ liquidityManagerContract,
|
|
|
+ account.address,
|
|
|
+ chain,
|
|
|
+ {
|
|
|
+ tokenId: liquidity0.tokenId,
|
|
|
+ liquidDelta: decLiquidity.toFixed(0),
|
|
|
+ minAmountX,
|
|
|
+ minAmountY
|
|
|
+ } as DecLiquidityParam,
|
|
|
+ gasPrice.toString()
|
|
|
+ )
|
|
|
+ const gasLimit = await decLiquidityCalling.estimateGas(options)
|
|
|
+ console.log('gas limit: ', gasLimit)
|
|
|
+ const tx0 = await wallet.sendTransaction({
|
|
|
+ from: wallet.address,
|
|
|
+ to: this.web3Configuration.liquidityManagerAddress,
|
|
|
+ data: decLiquidityCalling.encodeABI(),
|
|
|
+ gasLimit
|
|
|
+ })
|
|
|
+ console.log('decLiquidityCalling tx: ', JSON.stringify(tx0, null, 4))
|
|
|
+ }
|
|
|
+
|
|
|
+ async allowance(tokenAddress, spender, wallet: Wallet): Promise<number> {
|
|
|
+ const web3 = new Web3(new Web3.providers.HttpProvider(this.web3Configuration.zksyncRpcUrl))
|
|
|
+ const tokenAContract = new web3.eth.Contract(erc20, tokenAddress)
|
|
|
+ // @ts-ignore
|
|
|
+ const allowanceCalling = tokenAContract.methods.allowance(wallet.address, spender)
|
|
|
+ const out = await allowanceCalling.call()
|
|
|
+ Logger.log(`allowance: ${out} hex: ${Web3.utils.numberToHex(out + '')}`, 'GetAllowance')
|
|
|
+ return out as unknown as number
|
|
|
+ }
|
|
|
+
|
|
|
+ async approve(tokenAddress, spender, wallet: Wallet) {
|
|
|
+ const allowance = await this.allowance(tokenAddress, spender, wallet)
|
|
|
+ if (allowance > 0) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const web3 = new Web3(new Web3.providers.HttpProvider(this.web3Configuration.zksyncRpcUrl))
|
|
|
+ const tokenAContract = new web3.eth.Contract(erc20, tokenAddress)
|
|
|
+ // @ts-ignore
|
|
|
+ const approveCalling = tokenAContract.methods.approve(spender, '0xffffffffffffffffffffffffffffffff')
|
|
|
+ let gasLimit = await approveCalling.estimateGas({ from: wallet.address })
|
|
|
+ console.log('approve gas limit: ', gasLimit)
|
|
|
+
|
|
|
+ const tx0 = await wallet.sendTransaction({
|
|
|
+ from: wallet.address,
|
|
|
+ to: tokenAddress,
|
|
|
+ data: approveCalling.encodeABI(),
|
|
|
+ gasLimit
|
|
|
+ })
|
|
|
+ console.log('approve tx: ', JSON.stringify(tx0, null, 4))
|
|
|
+ }
|
|
|
+
|
|
|
+ async swapWithExactOutput(accountId, amount) {
|
|
|
+ const account = await this.accountService.findById(accountId)
|
|
|
+ const chainId = this.web3Configuration.ethereumNetwork == 'goerli' ? ChainId.ZkSyncAlphaTest : ChainId.ZkSyncEra
|
|
|
+ const chain = initialChainTable[chainId]
|
|
|
+ const provider = new Provider(this.web3Configuration.zksyncRpcUrl)
|
|
|
+ const wallet = new Wallet(account.privateKey).connect(provider)
|
|
|
+ const web3 = new Web3(new Web3.providers.HttpProvider(this.web3Configuration.zksyncRpcUrl))
|
|
|
+ console.log('address: ', wallet.address)
|
|
|
+
|
|
|
+ const balance = await web3.eth.getBalance(wallet.address)
|
|
|
+ console.log('ethBalance: ' + Web3.utils.fromWei(balance, 'ether'))
|
|
|
+
|
|
|
+ const fromToken = getGasToken(chainId)
|
|
|
+ console.log('fromToken: ', fromToken)
|
|
|
+ const toToken = await fetchToken(this.web3Configuration.tokenAddress, chain, web3 as any)
|
|
|
+ console.log('toToken: ', toToken)
|
|
|
+ const fee = 2000 // 2000 means 0.2%
|
|
|
+
|
|
|
+ const toTokenContract = getErc20TokenContract(toToken.address, web3 as any)
|
|
|
+
|
|
|
+ const toTokenBalance = await toTokenContract.methods.balanceOf(wallet.address).call()
|
|
|
+ console.log(toToken.symbol + ' balance: ' + amount2Decimal(new BigNumber(toTokenBalance), toToken))
|
|
|
+
|
|
|
+ const quoterContract = getQuoterContract(this.web3Configuration.quoterAddress, web3 as any)
|
|
|
+
|
|
|
+ const receiveAmount = new BigNumber(amount).times(10 ** toToken.decimal)
|
|
|
+
|
|
|
+ const params = {
|
|
|
+ // pay testA to buy testB
|
|
|
+ tokenChain: [fromToken, toToken],
|
|
|
+ feeChain: [fee],
|
|
|
+ outputAmount: receiveAmount.toFixed(0)
|
|
|
+ }
|
|
|
+
|
|
|
+ const { inputAmount } = await quoterSwapChainWithExactOutput(quoterContract, params)
|
|
|
+
|
|
|
+ const payAmount = inputAmount
|
|
|
+ const payAmountDecimal = amount2Decimal(new BigNumber(payAmount), fromToken)
|
|
|
+
|
|
|
+ console.log(toToken.symbol + ' to desired: ', amount)
|
|
|
+ console.log(fromToken.symbol + ' to pay: ', payAmountDecimal)
|
|
|
+
|
|
|
+ const swapContract = getSwapContract(this.web3Configuration.swapAddress, web3 as any)
|
|
|
+
|
|
|
+ const swapParams = {
|
|
|
+ ...params,
|
|
|
+ // slippery is 1.5%
|
|
|
+ maxInputAmount: new BigNumber(payAmount).times(1.015).toFixed(0),
|
|
|
+ strictERC20Token: false
|
|
|
+ }
|
|
|
+
|
|
|
+ const gasPrice = await web3.eth.getGasPrice()
|
|
|
+ const { swapCalling, options } = getSwapChainWithExactOutputCall(
|
|
|
+ swapContract,
|
|
|
+ wallet.address,
|
|
|
+ chain,
|
|
|
+ swapParams,
|
|
|
+ gasPrice.toString()
|
|
|
+ )
|
|
|
+ console.log(JSON.stringify({ swapCalling, options }, null, 4))
|
|
|
+ const gasLimit = await swapCalling.estimateGas(options)
|
|
|
+ console.log('gas limit: ', gasLimit)
|
|
|
+
|
|
|
+ const tx = await wallet.sendTransaction({
|
|
|
+ value: Web3.utils.numberToHex(options.value),
|
|
|
+ data: swapCalling.arguments[0][0],
|
|
|
+ to: this.web3Configuration.swapAddress,
|
|
|
+ gasLimit
|
|
|
+ })
|
|
|
+ console.log(tx)
|
|
|
+ }
|
|
|
}
|