xiongzhu 2 роки тому
батько
коміт
ca48995513

+ 7 - 1
.env

@@ -67,6 +67,12 @@ WX_NOTIFY_URL=https://gpt.izouma.com/api/notify/weixin
 REDIS_URI=redis://:3a4FSzu8ErP9$CQ&@147.139.168.134:6379/0
 
 ETHEREUM_NETWORK=goerli
+ETHEREUM_EXPLORER=https://goerli.etherscan.io
 INFURA_API_KEY=6e36ce031e304b79af723a0a28789083
 ZKSYNC_RPC_URL=https://testnet.era.zksync.dev
-ZKSYNC_CHAIN_ID=280
+ZKSYNC_CHAIN_ID=280
+ZKSYNC_EXPLORER=https://goerli.explorer.zksync.io
+LIQUIDITY_MANAGER_ADDRESS=0x25727b360604E1e6B440c3B25aF368F54fc580B6
+TOKEN_ADDRESS=0x0faF6df7054946141266420b43783387A78d82A9
+QUOTER_ADDRESS=0xE93D1d35a63f7C6b51ef46a27434375761a7Db28
+SWAP_ADDRESS=0x3040EE148D09e5B92956a64CDC78b49f48C0cDdc

+ 3 - 1
.env.production

@@ -67,6 +67,8 @@ WX_NOTIFY_URL=https://gpt.izouma.com/api/notify/weixin
 REDIS_URI=redis://:3a4FSzu8ErP9$CQ&@127.0.0.1:6379/0
 
 ETHEREUM_NETWORK=mainnet
+ETHEREUM_EXPLORER=https://etherscan.io
 INFURA_API_KEY=6e36ce031e304b79af723a0a28789083
 ZKSYNC_RPC_URL=https://mainnet.era.zksync.io
-ZKSYNC_CHAIN_ID=324
+ZKSYNC_CHAIN_ID=324
+ZKSYNC_EXPLORER=https://explorer.zksync.io

+ 4 - 4
fix.js

@@ -1,6 +1,6 @@
-const fs = require('fs')
+import  fs  from 'fs'
 
-const package = JSON.parse(fs.readFileSync('./node_modules/bignumber.js/package.json', 'utf8'))
-package.exports['./bignumber'] = './bignumber.js'
-fs.writeFileSync('./node_modules/bignumber.js/package.json', JSON.stringify(package, null, 2))
+const packageJson = JSON.parse(fs.readFileSync('./node_modules/bignumber.js/package.json', 'utf8'))
+packageJson.exports['./bignumber'] = './bignumber.js'
+fs.writeFileSync('./node_modules/bignumber.js/package.json', JSON.stringify(packageJson, null, 2))
 console.log('Fixed bignumber.js package.json')

+ 2 - 0
package.json

@@ -4,6 +4,7 @@
   "description": "An API Boilerplate to create a ready-to-use REST API in seconds with NestJS 9.x and Auth JWT System",
   "author": "Tony133",
   "license": "MIT",
+  "type": "commonjs",
   "scripts": {
     "prebuild": "rimraf dist",
     "build": "nest build",
@@ -94,6 +95,7 @@
     "@types/nodemailer-express-handlebars": "^4.0.2",
     "@types/supertest": "^2.0.12",
     "@types/uuid": "^9.0.1",
+    "@types/ws": "^8.5.5",
     "@typescript-eslint/eslint-plugin": "^5.53.0",
     "@typescript-eslint/parser": "^5.53.0",
     "eslint": "^8.34.0",

+ 7 - 1
src/filters/all-exceptions-filter.filter.ts

@@ -1,5 +1,6 @@
 import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger } from '@nestjs/common'
 import { HttpAdapterHost } from '@nestjs/core'
+import { ContractExecutionError } from 'web3'
 
 @Catch()
 export class AllExceptionsFilter implements ExceptionFilter {
@@ -25,8 +26,13 @@ export class AllExceptionsFilter implements ExceptionFilter {
                 res.message = res.message.join()
             }
         }
-        Logger.error(`status=${httpStatus}, path=${path}, msg=${res.message}`, 'GlobalExceptionFilter')
 
+        Logger.error(`status=${httpStatus}, path=${path}, msg=${res.message}`, 'GlobalExceptionFilter')
+        if (exception instanceof ContractExecutionError) {
+            const innerError = (exception as ContractExecutionError).innerError
+            Logger.error(`${innerError.message}`, 'GlobalExceptionFilter')
+        }
+        console.log((exception as any)?.stack)
         httpAdapter.reply(ctx.getResponse(), res, httpStatus)
     }
 }

+ 1 - 1
src/helpers/configure-swagger-docs.helper.ts

@@ -1,7 +1,7 @@
 import { INestApplication } from '@nestjs/common'
 import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'
 import { ConfigService } from '@nestjs/config'
-import * as basicAuth from 'express-basic-auth'
+import basicAuth from 'express-basic-auth'
 
 const SWAGGER_ENVS = ['local', 'dev', 'staging']
 

+ 2 - 2
src/shared/mailer/mailer.service.ts

@@ -1,8 +1,8 @@
 import { Injectable } from '@nestjs/common'
 import { createTransport } from 'nodemailer'
-import * as Mail from 'nodemailer/lib/mailer'
+import Mail from 'nodemailer/lib/mailer'
 import { ConfigService } from '@nestjs/config'
-import * as hbs from 'nodemailer-express-handlebars'
+import hbs from 'nodemailer-express-handlebars'
 
 @Injectable()
 export class MailerService {

+ 223 - 0
src/web3/erc20.json

@@ -0,0 +1,223 @@
+[
+    {
+      "constant": true,
+      "inputs": [],
+      "name": "name",
+      "outputs": [
+        {
+          "name": "",
+          "type": "string"
+        }
+      ],
+      "payable": false,
+      "stateMutability": "view",
+      "type": "function"
+    },
+    {
+      "constant": false,
+      "inputs": [
+        {
+          "name": "_spender",
+          "type": "address"
+        },
+        {
+          "name": "_value",
+          "type": "uint256"
+        }
+      ],
+      "name": "approve",
+      "outputs": [
+        {
+          "name": "",
+          "type": "bool"
+        }
+      ],
+      "payable": false,
+      "stateMutability": "nonpayable",
+      "type": "function"
+    },
+    {
+      "constant": true,
+      "inputs": [],
+      "name": "totalSupply",
+      "outputs": [
+        {
+          "name": "",
+          "type": "uint256"
+        }
+      ],
+      "payable": false,
+      "stateMutability": "view",
+      "type": "function"
+    },
+    {
+      "constant": false,
+      "inputs": [
+        {
+          "name": "_from",
+          "type": "address"
+        },
+        {
+          "name": "_to",
+          "type": "address"
+        },
+        {
+          "name": "_value",
+          "type": "uint256"
+        }
+      ],
+      "name": "transferFrom",
+      "outputs": [
+        {
+          "name": "",
+          "type": "bool"
+        }
+      ],
+      "payable": false,
+      "stateMutability": "nonpayable",
+      "type": "function"
+    },
+    {
+      "constant": true,
+      "inputs": [],
+      "name": "decimals",
+      "outputs": [
+        {
+          "name": "",
+          "type": "uint8"
+        }
+      ],
+      "payable": false,
+      "stateMutability": "view",
+      "type": "function"
+    },
+    {
+      "constant": true,
+      "inputs": [
+        {
+          "name": "_owner",
+          "type": "address"
+        }
+      ],
+      "name": "balanceOf",
+      "outputs": [
+        {
+          "name": "balance",
+          "type": "uint256"
+        }
+      ],
+      "payable": false,
+      "stateMutability": "view",
+      "type": "function"
+    },
+    {
+      "constant": true,
+      "inputs": [],
+      "name": "symbol",
+      "outputs": [
+        {
+          "name": "",
+          "type": "string"
+        }
+      ],
+      "payable": false,
+      "stateMutability": "view",
+      "type": "function"
+    },
+    {
+      "constant": false,
+      "inputs": [
+        {
+          "name": "_to",
+          "type": "address"
+        },
+        {
+          "name": "_value",
+          "type": "uint256"
+        }
+      ],
+      "name": "transfer",
+      "outputs": [
+        {
+          "name": "",
+          "type": "bool"
+        }
+      ],
+      "payable": false,
+      "stateMutability": "nonpayable",
+      "type": "function"
+    },
+    {
+      "constant": true,
+      "inputs": [
+        {
+          "name": "_owner",
+          "type": "address"
+        },
+        {
+          "name": "_spender",
+          "type": "address"
+        }
+      ],
+      "name": "allowance",
+      "outputs": [
+        {
+          "name": "",
+          "type": "uint256"
+        }
+      ],
+      "payable": false,
+      "stateMutability": "view",
+      "type": "function"
+    },
+    {
+      "payable": true,
+      "stateMutability": "payable",
+      "type": "fallback"
+    },
+    {
+      "anonymous": false,
+      "inputs": [
+        {
+          "indexed": true,
+          "name": "owner",
+          "type": "address"
+        },
+        {
+          "indexed": true,
+          "name": "spender",
+          "type": "address"
+        },
+        {
+          "indexed": false,
+          "name": "value",
+          "type": "uint256"
+        }
+      ],
+      "name": "Approval",
+      "type": "event"
+    },
+    {
+      "anonymous": false,
+      "inputs": [
+        {
+          "indexed": true,
+          "name": "from",
+          "type": "address"
+        },
+        {
+          "indexed": true,
+          "name": "to",
+          "type": "address"
+        },
+        {
+          "indexed": false,
+          "name": "value",
+          "type": "uint256"
+        }
+      ],
+      "name": "Transfer",
+      "type": "event"
+    }
+  ]
+  

+ 7 - 1
src/web3/web3.config.ts

@@ -8,7 +8,13 @@ const configService = new ConfigService()
 export default registerAs('web3', () => {
     return {
         ethereumNetwork: configService.get<string>('ETHEREUM_NETWORK'),
+        ethereumExplorer: configService.get<string>('ETHEREUM_EXPLORER'),
         infuraApiKey: configService.get<string>('INFURA_API_KEY'),
-        zksyncRpcUrl: configService.get<string>('ZKSYNC_RPC_URL')
+        zksyncRpcUrl: configService.get<string>('ZKSYNC_RPC_URL'),
+        zksyncExplorer: configService.get<string>('ZKSYNC_EXPLORER'),
+        liquidityManagerAddress: configService.get<string>('LIQUIDITY_MANAGER_ADDRESS'),
+        tokenAddress: configService.get<string>('TOKEN_ADDRESS'),
+        quoterAddress: configService.get<string>('QUOTER_ADDRESS'),
+        swapAddress: configService.get<string>('SWAP_ADDRESS'),
     }
 })

+ 21 - 6
src/web3/web3.controller.ts

@@ -5,13 +5,28 @@ import { Web3Service } from './web3.service'
 export class Web3Controller {
     constructor(private readonly web3Service: Web3Service) {}
 
-    @Post('/zk-deposite')
-    public async zkDeposite(@Body() { accountId, amount }) {
-        return await this.web3Service.zkDeposite(accountId, amount)
+    @Post('/zk-deposit')
+    public async zkDeposit(@Body() { accountId, amount }) {
+        return await this.web3Service.zkDeposit(accountId, amount)
     }
 
-    @Post('/zk-widthdraw')
-    public async zkWidthdraw() {
-        return await this.web3Service.zkWidthdraw()
+    @Post('/zk-withdraw')
+    public async zkWidthdraw(@Body() { accountId, amount }) {
+        return await this.web3Service.zkWidthdraw(accountId, amount)
+    }
+
+    @Post('/add-liquidity')
+    public async addLiquidity(@Body() { accountId, amount }) {
+        return await this.web3Service.addLiquidity(accountId, amount)
+    }
+
+    @Post('/remove-liquidity')
+    public async removeLiquidity(@Body() { accountId }) {
+        return await this.web3Service.removeLiquidity(accountId)
+    }
+
+    @Post('/swap-exact-out')
+    public async swap(@Body() { accountId, amount }) {
+        return await this.web3Service.swapWithExactOutput(accountId, amount)
     }
 }

+ 325 - 3
src/web3/web3.service.ts

@@ -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)
+    }
 }

+ 15 - 13
tsconfig.json

@@ -1,15 +1,17 @@
 {
-  "compilerOptions": {
-    "module": "commonjs",
-    "declaration": true,
-    "removeComments": true,
-    "emitDecoratorMetadata": true,
-    "experimentalDecorators": true,
-    "target": "es2017",
-    "sourceMap": true,
-    "outDir": "./dist",
-    "baseUrl": "./",
-    "incremental": true,
-  },
-  "exclude": ["node_modules", "dist"]
+    "compilerOptions": {
+        "module": "commonjs",
+        "declaration": true,
+        "removeComments": true,
+        "emitDecoratorMetadata": true,
+        "experimentalDecorators": true,
+        "target": "es2017",
+        "sourceMap": true,
+        "outDir": "./dist",
+        "baseUrl": "./",
+        "incremental": true,
+        "esModuleInterop": true,
+        "resolveJsonModule": true
+    },
+    "exclude": ["node_modules", "dist"]
 }

Різницю між файлами не показано, бо вона завелика
+ 583 - 16
yarn.lock


Деякі файли не було показано, через те що забагато файлів було змінено