diff --git a/package.json b/package.json index 60966e93..17f31e9c 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "release": "release-it --non-interactive", "changelog": "auto-changelog -p", "prepublishOnly": "npm run build", - "test:pool": "mocha --config=test/unit/.mocharc.json --node-env=test --exit 'test/unit/pools/Router.test.ts'", + "test:pool": "mocha --config=test/unit/.mocharc.json --node-env=test --exit 'test/unit/pools/balancer/Pool.test.ts'", + "test:router": "mocha --config=test/unit/.mocharc.json --node-env=test --exit 'test/unit/pools/Router.test.ts'", "test:unit": "mocha --config=test/unit/.mocharc.json --node-env=test --exit 'test/unit/**/*.test.ts'", "test:unit:cover": "nyc --report-dir coverage/unit npm run test:unit", "test:integration": "mocha --config=test/integration/.mocharc.json --node-env=test --exit 'test/integration/**/*.test.ts'", diff --git a/src/pools/Router.ts b/src/pools/Router.ts index 5aa2258e..4f00d4cb 100644 --- a/src/pools/Router.ts +++ b/src/pools/Router.ts @@ -122,10 +122,36 @@ export class Router { return await this.router.methods.isPoolTemplate(address).call() } + /** + * Estimate gas cost for addOceanToken + * @param {String} address + * @param {String} tokenAddress token address we want to add + * @param {Contract} routerContract optional contract instance + * @return {Promise} + */ + public async estGasAddOceanToken( + address: string, + tokenAddress: string, + contractInstance?: Contract + ) { + const routerContract = + contractInstance || this.router + + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await routerContract.methods + .addOceanToken(tokenAddress) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + return estGas + } /** * Add a new token to oceanTokens list, pools with basetoken in this list have NO opf Fee - * @param {String} address - * @param {String} tokenAddress template address to add + * @param {String} address caller address + * @param {String} tokenAddress token address to add * @return {Promise} */ public async addOceanToken( @@ -136,15 +162,9 @@ export class Router { throw new Error(`Caller is not Router Owner`) } - const gasLimitDefault = this.GASLIMIT_DEFAULT - let estGas - try { - estGas = await this.router.methods - .addOceanToken(tokenAddress) - .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) - } catch (e) { - estGas = gasLimitDefault - } + + const estGas = await this.estGasAddOceanToken(address,tokenAddress) + // Invoke createToken function of the contract const trxReceipt = await this.router.methods.addOceanToken(tokenAddress).send({ @@ -156,6 +176,33 @@ export class Router { return trxReceipt } + /** + * Estimate gas cost for removeOceanToken + * @param {String} address caller address + * @param {String} tokenAddress token address we want to add + * @param {Contract} routerContract optional contract instance + * @return {Promise} + */ + public async estGasRemoveOceanToken( + address: string, + tokenAddress: string, + contractInstance?: Contract + ) { + const routerContract = + contractInstance || this.router + + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await routerContract.methods + .removeOceanToken(tokenAddress) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + return estGas + } + /** * Remove a token from oceanTokens list, pools without basetoken in this list have a opf Fee * @param {String} address @@ -170,15 +217,8 @@ export class Router { throw new Error(`Caller is not Router Owner`) } - const gasLimitDefault = this.GASLIMIT_DEFAULT - let estGas - try { - estGas = await this.router.methods - .removeOceanToken(tokenAddress) - .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) - } catch (e) { - estGas = gasLimitDefault - } + const estGas = await this.estGasRemoveOceanToken(address,tokenAddress) + // Invoke createToken function of the contract const trxReceipt = await this.router.methods.removeOceanToken(tokenAddress).send({ diff --git a/src/pools/balancer/OceanPool.ts b/src/pools/balancer/OceanPool.ts deleted file mode 100644 index 5711f9a6..00000000 --- a/src/pools/balancer/OceanPool.ts +++ /dev/null @@ -1,38 +0,0 @@ -import Web3 from 'web3' -import { AbiItem } from 'web3-utils' -import { Contract } from 'web3-eth-contract' -import defaultPoolABI from '@oceanprotocol/contracts/artifacts/contracts/interfaces/IPool.sol/IPool.json' -import defaultERC20ABI from '@oceanprotocol/contracts/artifacts/contracts/interfaces/IERC20.sol/IERC20.json' -import { PoolFactory } from './PoolFactory' -import { Logger } from '../../utils' - -export class OceanPool extends PoolFactory { - public oceanAddress: string = null - public dtAddress: string = null - public startBlock: number - public vaultABI: AbiItem | AbiItem[] - public vaultAddress: string - public vault: Contract - public poolABI: AbiItem | AbiItem[] - public erc20ABI: AbiItem | AbiItem[] - - constructor( - web3: Web3, - logger: Logger, - routerAddress: string = null, - oceanAddress: string = null, - startBlock?: number - ) { - super(web3, logger, routerAddress) - - this.poolABI = defaultPoolABI.abi as AbiItem[] - this.erc20ABI = defaultERC20ABI.abi as AbiItem[] - this.vault = new this.web3.eth.Contract(this.vaultABI, this.vaultAddress) - - // if (oceanAddress) { - // this.oceanAddress = oceanAddress - // } - if (startBlock) this.startBlock = startBlock - else this.startBlock = 0 - } -} diff --git a/src/pools/balancer/Pool.ts b/src/pools/balancer/Pool.ts new file mode 100644 index 00000000..0dceabca --- /dev/null +++ b/src/pools/balancer/Pool.ts @@ -0,0 +1,1133 @@ +import Web3 from 'web3' +import { AbiItem } from 'web3-utils/types' +import { TransactionReceipt } from 'web3-core' +import { Contract } from 'web3-eth-contract' +import { Logger, getFairGasPrice } from '../../utils' +import BigNumber from 'bignumber.js' +import PoolTemplate from '@oceanprotocol/contracts/artifacts/contracts/pools/balancer/BPool.sol/BPool.json' +import defaultPool from '@oceanprotocol/contracts/artifacts/contracts/pools/FactoryRouter.sol/FactoryRouter.json' +import defaultERC20ABI from '@oceanprotocol/contracts/artifacts/contracts/templates/ERC20Template.sol/ERC20Template.json' +import Decimal from 'decimal.js' + +const MaxUint256 = + '115792089237316195423570985008687907853269984665640564039457584007913129639934' +/** + * Provides an interface to Ocean friendly fork from Balancer BPool + */ + + +export class Pool { + public poolABI: AbiItem | AbiItem[] + public web3: Web3 + public GASLIMIT_DEFAULT = 1000000 + private logger: Logger + + constructor( + web3: Web3, + logger: Logger, + poolABI: AbiItem | AbiItem[] = null + ) { + + if (poolABI) this.poolABI = poolABI + else this.poolABI = PoolTemplate.abi as AbiItem[] + this.web3 = web3 + this.logger = logger + } + + + /** + * Get Alloance for both DataToken and Ocean + * @param {String } tokenAdress + * @param {String} owner + * @param {String} spender + */ + public async allowance( + tokenAdress: string, + owner: string, + spender: string + ): Promise { + const tokenAbi = defaultERC20ABI.abi as AbiItem[] + const datatoken = new this.web3.eth.Contract(tokenAbi, tokenAdress, { + from: spender + }) + const trxReceipt = await datatoken.methods.allowance(owner, spender).call() + return this.web3.utils.fromWei(trxReceipt) + } + + /** + * Approve spender to spent amount tokens + * @param {String} account + * @param {String} tokenAddress + * @param {String} spender + * @param {String} amount (always expressed as wei) + * @param {String} force if true, will overwrite any previous allowence. Else, will check if allowence is enough and will not send a transaction if it's not needed + */ + async approve( + account: string, + tokenAddress: string, + spender: string, + amount: string, + force = false + ): Promise { + const minABI = [ + { + constant: false, + inputs: [ + { + name: '_spender', + type: 'address' + }, + { + name: '_value', + type: 'uint256' + } + ], + name: 'approve', + outputs: [ + { + name: '', + type: 'bool' + } + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function' + } + ] as AbiItem[] + const token = new this.web3.eth.Contract(minABI, tokenAddress, { + from: account + }) + if (!force) { + const currentAllowence = await this.allowance(tokenAddress, account, spender) + if ( + new Decimal(this.web3.utils.toWei(currentAllowence)).greaterThanOrEqualTo(amount) + ) { + return currentAllowence + } + } + let result = null + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await token.methods + .approve(spender, amount) + .estimateGas({ from: account }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + + try { + result = await token.methods.approve(spender, amount).send({ + from: account, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + } catch (e) { + this.logger.error(`ERRPR: Failed to approve spender to spend tokens : ${e.message}`) + } + return result + } + + /** + * Get user shares of pool tokens + * @param {String} account + * @param {String} poolAddress + * @return {String} + */ + async sharesBalance(account: string, poolAddress: string): Promise { + let result = null + try { + const token = new this.web3.eth.Contract(this.poolABI, poolAddress) + const balance = await token.methods.balanceOf(account).call() + result = this.web3.utils.fromWei(balance) + } catch (e) { + this.logger.error(`ERROR: Failed to get shares of pool : ${e.message}`) + } + return result + } + + + /** + * Set pool fee + * @param {String} account + * @param {String} poolAddress + * @param {String} fee 0.1=10% fee(max allowed) + */ + async setSwapFee( + account: string, + poolAddress: string, + fee: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress, { + from: account + }) + let result = null + try { + result = await pool.methods.setSwapFee(this.web3.utils.toWei(fee)).send({ + from: account, + gas: this.GASLIMIT_DEFAULT, + gasPrice: await getFairGasPrice(this.web3) + }) + } catch (e) { + this.logger.error(`ERROR: Failed to set pool swap fee: ${e.message}`) + } + return result + } + + + /** + * Get number of tokens composing this pool + * @param {String} poolAddress + * @return {String} + */ + async getNumTokens(poolAddress: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + try { + result = await pool.methods.getNumTokens().call() + } catch (e) { + this.logger.error(`ERROR: Failed to get number of tokens: ${e.message}`) + } + return result + } + + /** + * Get total supply of pool shares + * @param {String} poolAddress + * @return {String} + */ + async getPoolSharesTotalSupply(poolAddress: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let amount = null + try { + const result = await pool.methods.totalSupply().call() + amount = this.web3.utils.fromWei(result) + } catch (e) { + this.logger.error(`ERROR: Failed to get total supply of pool shares: ${e.message}`) + } + return amount + } + + /** + * Get tokens composing this pool + * @param {String} poolAddress + * @return {String[]} + */ + async getCurrentTokens(poolAddress: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + try { + result = await pool.methods.getCurrentTokens().call() + } catch (e) { + this.logger.error(`ERROR: Failed to get tokens composing this pool: ${e.message}`) + } + return result + } + + /** + * Get the final tokens composing this pool + * @param {String} poolAddress + * @return {String[]} + */ + async getFinalTokens(poolAddress: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + try { + result = await pool.methods.getFinalTokens().call() + } catch (e) { + this.logger.error(`ERROR: Failed to get the final tokens composing this pool`) + } + return result + } + + /** + * Get controller address of this pool + * @param {String} poolAddress + * @return {String} + */ + async getController(poolAddress: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + try { + result = await pool.methods.getController().call() + } catch (e) { + this.logger.error(`ERROR: Failed to get pool controller address: ${e.message}`) + } + return result + } + + + /** + * Get if a token is bounded to a pool + * @param {String} poolAddress + * @param {String} token Address of the token + * @return {Boolean} + */ + async isBound(poolAddress: string, token: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + try { + result = await pool.methods.isBound(token).call() + } catch (e) { + this.logger.error(`ERROR: Failed to check whether a token \ + bounded to a pool. ${e.message}`) + } + return result + } + + /** + * Get how many tokens are in the pool + * @param {String} poolAddress + * @param {String} token Address of the token + * @return {String} + */ + async getReserve(poolAddress: string, token: string): Promise { + let amount = null + try { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + const result = await pool.methods.getBalance(token).call() + amount = this.web3.utils.fromWei(result) + } catch (e) { + this.logger.error(`ERROR: Failed to get how many tokens \ + are in the pool: ${e.message}`) + } + return amount + } + + /** + * Get if a pool is finalized + * @param {String} poolAddress + * @return {Boolean} + */ + async isFinalized(poolAddress: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + try { + result = await pool.methods.isFinalized().call() + } catch (e) { + this.logger.error(`ERROR: Failed to check whether pool is finalized: ${e.message}`) + } + return result + } + + /** + * Get pool fee + * @param {String} poolAddress + * @return {String} Swap fee. To get the percentage value, substract by 100. E.g. `0.1` represents a 10% swap fee. + */ + async getSwapFee(poolAddress: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let fee = null + try { + const result = await pool.methods.getSwapFee().call() + fee = this.web3.utils.fromWei(result) + } catch (e) { + this.logger.error(`ERROR: Failed to get pool fee: ${e.message}`) + } + return fee + } + + /** + * The normalized weight of a token. The combined normalized weights of all tokens will sum up to 1. (Note: the actual sum may be 1 plus or minus a few wei due to division precision loss) + * @param {String} poolAddress + * @param {String} token + * @return {String} + */ + async getNormalizedWeight(poolAddress: string, token: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let weight = null + try { + const result = await pool.methods.getNormalizedWeight(token).call() + weight = this.web3.utils.fromWei(result) + } catch (e) { + this.logger.error(`ERROR: Failed to get normalized weight of a token: ${e.message}`) + } + return weight + } + + /** + * getDenormalizedWeight of a token in pool + * @param {String} poolAddress + * @param {String} token + * @return {String} + */ + async getDenormalizedWeight(poolAddress: string, token: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let weight = null + try { + const result = await pool.methods.getDenormalizedWeight(token).call() + weight = this.web3.utils.fromWei(result) + } catch (e) { + this.logger.error('ERROR: Failed to get denormalized weight of a token in pool') + } + return weight + } + + /** + * getTotalDenormalizedWeight in pool + * @param {String} poolAddress + * @return {String} + */ + async getTotalDenormalizedWeight(poolAddress: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let weight = null + try { + const result = await pool.methods.getTotalDenormalizedWeight().call() + weight = this.web3.utils.fromWei(result) + } catch (e) { + this.logger.error('ERROR: Failed to get total denormalized weight in pool') + } + return weight + } + + /** + * Estimate gas cost for swapExactAmountIn + * @param {String} address + * @param {String} poolAddress + * @param {String} tokenIn + * @param {String} tokenAmountIn will be converted to wei + * @param {String} tokenOut + * @param {String} minAmountOut will be converted to wei + * @param {String} maxPrice will be converted to wei + * @param {Contract} contractInstance optional contract instance + * @return {Promise} + */ + public async estSwapExactAmountIn( + address: string, + poolAddress: string, + tokenIn: string, + tokenAmountIn: string, + tokenOut: string, + minAmountOut: string, + maxPrice: string, + contractInstance?: Contract + ) { + const poolContract = + contractInstance || new this.web3.eth.Contract(this.poolABI as AbiItem[],poolAddress) + + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await poolContract.methods + .swapExactAmountIn( + tokenIn, + this.web3.utils.toWei(tokenAmountIn), + tokenOut, + this.web3.utils.toWei(minAmountOut), + maxPrice ? this.web3.utils.toWei(maxPrice) : MaxUint256 + ) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + return estGas + } + + /** + * swapExactAmountIn - Trades an exact tokenAmountIn of tokenIn taken from the caller by the pool, in exchange for at least minAmountOut of tokenOut given to the caller from the pool, with a maximum marginal price of maxPrice. Returns (tokenAmountOut, spotPriceAfter), where tokenAmountOut is the amount of token that came out of the pool, and spotPriceAfter is the new marginal spot price, ie, the result of getSpotPrice after the call. (These values are what are limited by the arguments; you are guaranteed tokenAmountOut >= minAmountOut and spotPriceAfter <= maxPrice). + * @param {String} address + * @param {String} poolAddress + * @param {String} tokenIn + * @param {String} tokenAmountIn will be converted to wei + * @param {String} tokenOut + * @param {String} minAmountOut will be converted to wei + * @param {String} maxPrice will be converted to wei + * @return {TransactionReceipt} + */ + async swapExactAmountIn( + address: string, + poolAddress: string, + tokenIn: string, + tokenAmountIn: string, + tokenOut: string, + minAmountOut: string, + maxPrice?: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + // TODO: add multiple decimals support + const estGas = await this.estSwapExactAmountIn(address,poolAddress,tokenIn, + this.web3.utils.toWei(tokenAmountIn), + tokenOut, + this.web3.utils.toWei(minAmountOut), + maxPrice ? this.web3.utils.toWei(maxPrice) : MaxUint256) + + try { + result = await pool.methods + .swapExactAmountIn( + tokenIn, + this.web3.utils.toWei(tokenAmountIn), + tokenOut, + this.web3.utils.toWei(minAmountOut), + maxPrice ? this.web3.utils.toWei(maxPrice) : MaxUint256 + ) + .send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + } catch (e) { + this.logger.error(`ERROR: Failed to swap exact amount in : ${e.message}`) + } + return result + } + + /** + * Estimate gas cost for swapExactAmountOut + * @param {String} address + * @param {String} poolAddress + * @param {String} tokenIn + * @param {String} tokenAmountIn will be converted to wei + * @param {String} tokenOut + * @param {String} minAmountOut will be converted to wei + * @param {String} maxPrice will be converted to wei + * @param {Contract} contractInstance optional contract instance + * @return {Promise} + */ + public async estSwapExactAmountOut( + address: string, + poolAddress: string, + tokenIn: string, + maxAmountIn: string, + tokenOut: string, + amountOut: string, + maxPrice?: string, + contractInstance?: Contract + ) { + const poolContract = + contractInstance || new this.web3.eth.Contract(this.poolABI as AbiItem[],poolAddress) + + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await poolContract.methods + .swapExactAmountOut( + tokenIn, + this.web3.utils.toWei(maxAmountIn), + tokenOut, + this.web3.utils.toWei(amountOut), + maxPrice ? this.web3.utils.toWei(maxPrice) : MaxUint256 + ) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + return estGas + } + + /** + * swapExactAmountOut + * @param {String} account + * @param {String} poolAddress + * @param {String} tokenIn + * @param {String} maxAmountIn will be converted to wei + * @param {String} tokenOut + * @param {String} amountOut will be converted to wei + * @param {String} maxPrice will be converted to wei + * @return {TransactionReceipt} + */ + async swapExactAmountOut( + account: string, + poolAddress: string, + tokenIn: string, + maxAmountIn: string, + tokenOut: string, + amountOut: string, + maxPrice?: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + + const estGas = await this.estSwapExactAmountOut(account,poolAddress,tokenIn, + this.web3.utils.toWei(maxAmountIn), + tokenOut, + this.web3.utils.toWei(amountOut), + maxPrice ? this.web3.utils.toWei(maxPrice) : MaxUint256) + + try { + result = await pool.methods + .swapExactAmountOut( + tokenIn, + this.web3.utils.toWei(maxAmountIn), + tokenOut, + this.web3.utils.toWei(amountOut), + maxPrice ? this.web3.utils.toWei(maxPrice) : MaxUint256 + ) + .send({ + from: account, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + } catch (e) { + this.logger.error(`ERROR: Failed to swap exact amount out: ${e.message}`) + } + return result + } + + /** + * Join the pool, getting poolAmountOut pool tokens. This will pull some of each of the currently trading tokens in the pool, meaning you must have called approve for each token for this pool. These values are limited by the array of maxAmountsIn in the order of the pool tokens. + * @param {String} account + * @param {String} poolAddress + * @param {String} poolAmountOut will be converted to wei + * @param {String[]} maxAmountsIn array holding maxAmount per each token, will be converted to wei + * @return {TransactionReceipt} + */ + async joinPool( + account: string, + poolAddress: string, + poolAmountOut: string, + maxAmountsIn: string[] + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress, { + from: account + }) + const weiMaxAmountsIn = [] + + let amount: string + + for (amount of maxAmountsIn) { + weiMaxAmountsIn.push(this.web3.utils.toWei(amount)) + } + + let result = null + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await pool.methods + .joinPool(this.web3.utils.toWei(poolAmountOut), weiMaxAmountsIn) + .estimateGas({ from: account }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + try { + result = await pool.methods + .joinPool(this.web3.utils.toWei(poolAmountOut), weiMaxAmountsIn) + .send({ + from: account, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + } catch (e) { + this.logger.error(`ERROR: Failed to join pool: ${e.message}`) + } + return result + } + + /** + * Exit the pool, paying poolAmountIn pool tokens and getting some of each of the currently trading tokens in return. These values are limited by the array of minAmountsOut in the order of the pool tokens. + * @param {String} account + * @param {String} poolAddress + * @param {String} poolAmountIn will be converted to wei + * @param {String[]} minAmountsOut array holding minAmount per each token, will be converted to wei + * @return {TransactionReceipt} + */ + async exitPool( + account: string, + poolAddress: string, + poolAmountIn: string, + minAmountsOut: string[] + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress, { + from: account + }) + const weiMinAmountsOut = [] + let amount: string + + for (amount of minAmountsOut) { + weiMinAmountsOut.push(this.web3.utils.toWei(amount)) + } + let result = null + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await pool.methods + .exitPool(this.web3.utils.toWei(poolAmountIn), weiMinAmountsOut) + .estimateGas({ from: account }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + try { + result = await pool.methods + .exitPool(this.web3.utils.toWei(poolAmountIn), weiMinAmountsOut) + .send({ from: account, gas: estGas, gasPrice: await getFairGasPrice(this.web3) }) + } catch (e) { + this.logger.error(`ERROR: Failed to exit pool: ${e.message}`) + } + return result + } + + /** + * Pay tokenAmountIn of token tokenIn to join the pool, getting poolAmountOut of the pool shares. + * @param {String} account + * @param {String} poolAddress + * @param {String} tokenIn + * @param {String} tokenAmountIn will be converted to wei + * @param {String} minPoolAmountOut will be converted to wei + * @return {TransactionReceipt} + */ + async joinswapExternAmountIn( + account: string, + poolAddress: string, + tokenIn: string, + tokenAmountIn: string, + minPoolAmountOut: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress, { + from: account + }) + let result = null + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await pool.methods + .joinswapExternAmountIn( + tokenIn, + this.web3.utils.toWei(tokenAmountIn), + this.web3.utils.toWei(minPoolAmountOut) + ) + .estimateGas({ from: account }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + try { + result = await pool.methods + .joinswapExternAmountIn( + tokenIn, + this.web3.utils.toWei(tokenAmountIn), + this.web3.utils.toWei(minPoolAmountOut) + ) + .send({ + from: account, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + } catch (e) { + this.logger.error(`ERROR: Failed to pay tokens in order to \ + join the pool: ${e.message}`) + } + return result + } + + /** + * Specify poolAmountOut pool shares that you want to get, and a token tokenIn to pay with. This costs tokenAmountIn tokens (these went into the pool). + * @param {String} account + * @param {String} poolAddress + * @param {String} tokenIn + * @param {String} poolAmountOut will be converted to wei + * @param {String} maxAmountIn will be converted to wei + * @return {TransactionReceipt} + */ + async joinswapPoolAmountOut( + account: string, + poolAddress: string, + tokenIn: string, + poolAmountOut: string, + maxAmountIn: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress, { + from: account + }) + let result = null + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await pool.methods + .joinswapPoolAmountOut( + tokenIn, + this.web3.utils.toWei(poolAmountOut), + this.web3.utils.toWei(maxAmountIn) + ) + .estimateGas({ from: account }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + try { + result = await pool.methods + .joinswapPoolAmountOut( + tokenIn, + this.web3.utils.toWei(poolAmountOut), + this.web3.utils.toWei(maxAmountIn) + ) + .send({ + from: account, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + } catch (e) { + this.logger.error('ERROR: Failed to join swap pool amount out') + } + return result + } + + /** + * Pay poolAmountIn pool shares into the pool, getting minTokenAmountOut of the given token tokenOut out of the pool. + * @param {String} account + * @param {String} poolAddress + * @param {String} tokenOut + * @param {String} poolAmountIn will be converted to wei + * @param {String} minTokenAmountOut will be converted to wei + * @return {TransactionReceipt} + */ + async exitswapPoolAmountIn( + account: string, + poolAddress: string, + tokenOut: string, + poolAmountIn: string, + minTokenAmountOut: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress, { + from: account + }) + let result = null + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await pool.methods + .exitswapPoolAmountIn( + tokenOut, + this.web3.utils.toWei(poolAmountIn), + this.web3.utils.toWei(minTokenAmountOut) + ) + .estimateGas({ from: account }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + try { + result = await pool.methods + .exitswapPoolAmountIn( + tokenOut, + this.web3.utils.toWei(poolAmountIn), + this.web3.utils.toWei(minTokenAmountOut) + ) + .send({ + from: account, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + } catch (e) { + this.logger.error(`ERROR: Failed to pay pool shares into the pool: ${e.message}`) + } + return result + } + + /** + * Specify tokenAmountOut of token tokenOut that you want to get out of the pool. This costs poolAmountIn pool shares (these went into the pool). + * @param {String} account + * @param {String} poolAddress + * @param {String} tokenOut + * @param {String} tokenAmountOut will be converted to wei + * @param {String} maxPoolAmountIn will be converted to wei + * @return {TransactionReceipt} + */ + async exitswapExternAmountOut( + account: string, + poolAddress: string, + tokenOut: string, + tokenAmountOut: string, + maxPoolAmountIn: string + ): Promise { + const gasLimitDefault = this.GASLIMIT_DEFAULT + + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress, { + from: account + }) + let result = null + let estGas + + try { + estGas = await pool.methods + .exitswapExternAmountOut( + tokenOut, + this.web3.utils.toWei(tokenAmountOut), + this.web3.utils.toWei(maxPoolAmountIn) + ) + .estimateGas({ from: account }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + + try { + result = await pool.methods + .exitswapExternAmountOut( + tokenOut, + this.web3.utils.toWei(tokenAmountOut), + this.web3.utils.toWei(maxPoolAmountIn) + ) + .send({ + from: account, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + } catch (e) { + this.logger.error('ERROR: Failed to exitswapExternAmountOut') + } + return result + } + + /** + * Get Spot Price of swaping tokenIn to tokenOut + * @param {String} poolAddress + * @param {String} tokenIn + * @param {String} tokenOut + * @return {String} + */ + async getSpotPrice( + poolAddress: string, + tokenIn: string, + tokenOut: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let price = null + try { + const result = await pool.methods.getSpotPrice(tokenIn, tokenOut).call() + price = this.web3.utils.fromWei(result) + } catch (e) { + this.logger.error('ERROR: Failed to get spot price of swapping tokenIn to tokenOut') + } + return price + } + + +// public async calcSpotPrice( +// poolAddress: string, +// tokenBalanceIn: string, +// tokenWeightIn: string, +// tokenBalanceOut: string, +// tokenWeightOut: string, +// swapFee: string +// ): Promise { +// const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) +// let amount = '0' +// try { +// const result = await pool.methods +// .calcSpotPrice( +// this.web3.utils.toWei(tokenBalanceIn), +// this.web3.utils.toWei(tokenWeightIn), +// this.web3.utils.toWei(tokenBalanceOut), +// this.web3.utils.toWei(tokenWeightOut), +// this.web3.utils.toWei(swapFee) +// ) +// .call() +// amount = this.web3.utils.fromWei(result) +// } catch (e) { +// this.logger.error('ERROR: Failed to call calcSpotPrice') +// } +// return amount +// } + + public async getAmountInExactOut( + poolAddress: string, + tokenIn: string, + tokenOut: string, + tokenAmountOut: string, + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let amount = null + // if (new Decimal(tokenAmountOut).gte(tokenBalanceOut)) return null + try { + const result = await pool.methods + . getAmountInExactOut( + tokenIn, + tokenOut, + tokenAmountOut + ) + .call() + // amount = this.web3.utils.fromWei(result) + } catch (e) { + this.logger.error('ERROR: Failed to calcInGivenOut') + } + return amount + } + + public async getAmountOutExactIn( + poolAddress: string, + tokenIn: string, + tokenOut: string, + tokenAmountIn: string, + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + const tokenInContract = new this.web3.eth.Contract(defaultERC20ABI.abi as AbiItem[], tokenIn) + let amountInFormatted + let tokenInDecimals + try { + tokenInDecimals = await tokenInContract.methods.decimals().call() + } catch(e) { + this.logger.error('ERROR: FAILED TO CALL DECIMALS()') + } + //const tokenInDecimals = await tokenInContract.methods.decimals().call() + if ( tokenInDecimals == 18){ + amountInFormatted = this.web3.utils.toWei(tokenAmountIn) + } else { + amountInFormatted = parseInt(tokenAmountIn)*10**(tokenInDecimals) + } + let amount = null + try { + const result = await pool.methods + .getAmountOutExactIn( + tokenIn, + tokenOut, + amountInFormatted + ) + .call() + amount = this.web3.utils.fromWei(result) + } catch (e) { + this.logger.error('ERROR: Failed to calcOutGivenIn') + } + return amount + } + + public async calcPoolOutGivenSingleIn( + poolAddress: string, + tokenBalanceIn: string, + tokenWeightIn: string, + poolSupply: string, + totalWeight: string, + tokenAmountIn: string, + swapFee: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let amount = null + try { + const result = await pool.methods + .calcPoolOutGivenSingleIn( + this.web3.utils.toWei(tokenBalanceIn), + this.web3.utils.toWei(tokenWeightIn), + this.web3.utils.toWei(poolSupply), + this.web3.utils.toWei(totalWeight), + this.web3.utils.toWei(tokenAmountIn), + this.web3.utils.toWei(swapFee) + ) + .call() + amount = this.web3.utils.fromWei(result) + } catch (e) { + this.logger.error(`ERROR: Failed to calculate PoolOutGivenSingleIn : ${e.message}`) + } + return amount + } + + public async calcSingleInGivenPoolOut( + poolAddress: string, + tokenBalanceIn: string, + tokenWeightIn: string, + poolSupply: string, + totalWeight: string, + poolAmountOut: string, + swapFee: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let amount = null + try { + const result = await pool.methods + .calcSingleInGivenPoolOut( + this.web3.utils.toWei(tokenBalanceIn), + this.web3.utils.toWei(tokenWeightIn), + this.web3.utils.toWei(poolSupply), + this.web3.utils.toWei(totalWeight), + this.web3.utils.toWei(poolAmountOut), + this.web3.utils.toWei(swapFee) + ) + .call() + amount = this.web3.utils.fromWei(result) + } catch (e) { + this.logger.error(`ERROR: Failed to calculate SingleInGivenPoolOut : ${e.message}`) + } + return amount + } + + public async calcSingleOutGivenPoolIn( + poolAddress: string, + tokenBalanceOut: string, + tokenWeightOut: string, + poolSupply: string, + totalWeight: string, + poolAmountIn: string, + swapFee: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let amount = null + try { + const result = await pool.methods + .calcSingleOutGivenPoolIn( + this.web3.utils.toWei(tokenBalanceOut), + this.web3.utils.toWei(tokenWeightOut), + this.web3.utils.toWei(poolSupply), + this.web3.utils.toWei(totalWeight), + this.web3.utils.toWei(poolAmountIn), + this.web3.utils.toWei(swapFee) + ) + .call() + amount = this.web3.utils.fromWei(result) + } catch (e) { + this.logger.error(`ERROR: Failed to calculate SingleOutGivenPoolIn : ${e.message}`) + } + return amount + } + + public async calcPoolInGivenSingleOut( + poolAddress: string, + tokenBalanceOut: string, + tokenWeightOut: string, + poolSupply: string, + totalWeight: string, + tokenAmountOut: string, + swapFee: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let amount = null + try { + const result = await pool.methods + .calcPoolInGivenSingleOut( + this.web3.utils.toWei(tokenBalanceOut), + this.web3.utils.toWei(tokenWeightOut), + this.web3.utils.toWei(poolSupply), + this.web3.utils.toWei(totalWeight), + this.web3.utils.toWei(tokenAmountOut), + this.web3.utils.toWei(swapFee) + ) + .call() + amount = this.web3.utils.fromWei(result) + } catch (e) { + this.logger.error(`ERROR: Failed to calculate PoolInGivenSingleOut : ${e.message}`) + } + return amount + } + + /** + * Get LOG_SWAP encoded topic + * @return {String} + */ + public getSwapEventSignature(): string { + const abi = this.poolABI as AbiItem[] + const eventdata = abi.find(function (o) { + if (o.name === 'LOG_SWAP' && o.type === 'event') return o + }) + const topic = this.web3.eth.abi.encodeEventSignature(eventdata as any) + return topic + } + + /** + * Get LOG_JOIN encoded topic + * @return {String} + */ + public getJoinEventSignature(): string { + const abi = this.poolABI as AbiItem[] + const eventdata = abi.find(function (o) { + if (o.name === 'LOG_JOIN' && o.type === 'event') return o + }) + const topic = this.web3.eth.abi.encodeEventSignature(eventdata as any) + return topic + } + + /** + * Get LOG_EXIT encoded topic + * @return {String} + */ + public getExitEventSignature(): string { + const abi = this.poolABI as AbiItem[] + const eventdata = abi.find(function (o) { + if (o.name === 'LOG_EXIT' && o.type === 'event') return o + }) + const topic = this.web3.eth.abi.encodeEventSignature(eventdata as any) + return topic + } +} \ No newline at end of file diff --git a/src/pools/balancer/PoolFactory.ts b/src/pools/balancer/PoolFactory.ts deleted file mode 100644 index 1b74b57c..00000000 --- a/src/pools/balancer/PoolFactory.ts +++ /dev/null @@ -1,58 +0,0 @@ -import Web3 from 'web3' -import { AbiItem } from 'web3-utils' -import { Contract } from 'web3-eth-contract' -import defaultRouterABI from '@oceanprotocol/contracts/artifacts/contracts/interfaces/IFactoryRouter.sol/IFactoryRouter.json' -import { Logger } from '../../utils' -import { TransactionReceipt } from 'web3-eth' - -export class PoolFactory { - public GASLIMIT_DEFAULT = 1000000 - public web3: Web3 = null - public routerABI: AbiItem | AbiItem[] - - public routerAddress: string - - public logger: Logger - public router: Contract - - /** - * Instantiate PoolFactory. - * @param {String} routerAddress - * @param {AbiItem | AbiItem[]} routerABI - * @param {Web3} web3 - */ - constructor( - web3: Web3, - logger: Logger, - routerAddress: string, - routerABI?: AbiItem | AbiItem[] - ) { - this.web3 = web3 - this.routerAddress = routerAddress - this.routerABI = routerABI || (defaultRouterABI.abi as AbiItem[]) - this.logger = logger - this.router = new this.web3.eth.Contract(this.routerABI, this.routerAddress) - } - - public async deployPool( - account: string, - tokens: string[], - weightsInWei: string[], - swapFeePercentage: number, - swapMarketFee: number, - owner: string - ): Promise { - const gasLimitDefault = this.GASLIMIT_DEFAULT - let estGas - try { - estGas = await this.router.methods - .deployPool(tokens, weightsInWei, swapFeePercentage, swapMarketFee, owner) - .estimateGas({ from: account }, (err, estGas) => (err ? gasLimitDefault : estGas)) - } catch (e) { - this.logger.log('Error estimate gas deployPool') - this.logger.log(e) - estGas = gasLimitDefault - } - return estGas - } -} diff --git a/test/unit/pools/balancer/Pool.test.ts b/test/unit/pools/balancer/Pool.test.ts new file mode 100644 index 00000000..03b613bf --- /dev/null +++ b/test/unit/pools/balancer/Pool.test.ts @@ -0,0 +1,160 @@ +import { assert, expect } from 'chai' +import { AbiItem } from 'web3-utils/types' +import { TestContractHandler } from '../../../TestContractHandler' +import Web3 from 'web3' +import ERC721Factory from '@oceanprotocol/contracts/artifacts/contracts/ERC721Factory.sol/ERC721Factory.json' +import ERC721Template from '@oceanprotocol/contracts/artifacts/contracts/templates/ERC721Template.sol/ERC721Template.json' +import SideStaking from '@oceanprotocol/contracts/artifacts/contracts/pools/ssContracts/SideStaking.sol/SideStaking.json' +import FactoryRouter from '@oceanprotocol/contracts/artifacts/contracts/pools/FactoryRouter.sol/FactoryRouter.json' +import ERC20Template from '@oceanprotocol/contracts/artifacts/contracts/templates/ERC20Template.sol/ERC20Template.json' +import Dispenser from '@oceanprotocol/contracts/artifacts/contracts/pools/dispenser/Dispenser.sol/Dispenser.json' +import FixedRate from '@oceanprotocol/contracts/artifacts/contracts/pools/fixedRate/FixedRateExchange.sol/FixedRateExchange.json' +import MockERC20 from '@oceanprotocol/contracts/artifacts/contracts/utils/mock/MockERC20Decimals.sol/MockERC20Decimals.json' +import PoolTemplate from '@oceanprotocol/contracts/artifacts/contracts/pools/balancer/BPool.sol/BPool.json' +import { LoggerInstance } from '../../../../src/utils' +import { NFTFactory } from '../../../../src/factories/NFTFactory' +import { Pool } from '../../../../src/pools/balancer/Pool' +const { keccak256 } = require('@ethersproject/keccak256') +const web3 = new Web3('http://127.0.0.1:8545') +const communityCollector = '0xeE9300b7961e0a01d9f0adb863C7A227A07AaD75' + +describe('Pool unit test', () => { + let factoryOwner: string + let nftOwner: string + let user1: string + let user2: string + let user3: string + let contracts: TestContractHandler + let pool: Pool + let dtAddress: string + let dtAddress2: string + let poolAddress: string + let erc20Token: string + + it('should deploy contracts', async () => { + contracts = new TestContractHandler( + web3, + ERC721Template.abi as AbiItem[], + ERC20Template.abi as AbiItem[], + PoolTemplate.abi as AbiItem[], + ERC721Factory.abi as AbiItem[], + FactoryRouter.abi as AbiItem[], + SideStaking.abi as AbiItem[], + FixedRate.abi as AbiItem[], + Dispenser.abi as AbiItem[], + + ERC721Template.bytecode, + ERC20Template.bytecode, + PoolTemplate.bytecode, + ERC721Factory.bytecode, + FactoryRouter.bytecode, + SideStaking.bytecode, + FixedRate.bytecode, + Dispenser.bytecode + ) + await contracts.getAccounts() + factoryOwner = contracts.accounts[0] + nftOwner = contracts.accounts[1] + user1 = contracts.accounts[2] + user2 = contracts.accounts[3] + user3 = contracts.accounts[4] + + await contracts.deployContracts(factoryOwner, FactoryRouter.abi as AbiItem[]) + + const daiContract = new web3.eth.Contract( + contracts.MockERC20.options.jsonInterface, + contracts.daiAddress + ) + await daiContract.methods + .approve(contracts.factory721Address, web3.utils.toWei('10000')) + .send({ from: contracts.accounts[0] }) + + expect(await daiContract.methods.balanceOf(contracts.accounts[0]).call()).to.equal( + web3.utils.toWei('100000') + ) + }) + + it('should initiate Pool instance', async () => { + pool = new Pool(web3, LoggerInstance,PoolTemplate.abi as AbiItem[]) + + }) + + + it('#create a pool', async () => { + // CREATE A POOL + // we prepare transaction parameters objects + const nftData = { + name: '72120Bundle', + symbol: '72Bundle', + templateIndex: 1, + baseURI: 'https://oceanprotocol.com/nft/' + } + const ercData = { + templateIndex: 1, + strings: ['ERC20B1', 'ERC20DT1Symbol'], + addresses: [ + contracts.accounts[0], + user3, + contracts.accounts[0], + '0x0000000000000000000000000000000000000000' + ], + uints: [web3.utils.toWei('1000000'), 0], + bytess: [] + } + + const poolData = { + addresses: [ + contracts.sideStakingAddress, + contracts.daiAddress, + contracts.factory721Address, + contracts.accounts[0], + contracts.accounts[0], + contracts.poolTemplateAddress + ], + ssParams: [ + web3.utils.toWei('1'), // rate + 18, // basetokenDecimals + web3.utils.toWei('10000'), + 2500000, // vested blocks + web3.utils.toWei('2000') // baseToken initial pool liquidity + ], + swapFees: [ + 1e15, // + 1e15 + ] + } + + const nftFactory = new NFTFactory(contracts.factory721Address, web3, LoggerInstance) + + const txReceipt = await nftFactory.createNftErcWithPool( + contracts.accounts[0], + nftData, + ercData, + poolData + ) + + erc20Token = txReceipt.events.TokenCreated.returnValues.newTokenAddress + poolAddress = txReceipt.events.NewPool.returnValues.poolAddress + + + + const erc20Contract = new web3.eth.Contract( + ERC20Template.abi as AbiItem[], + erc20Token + ) + // user2 has no dt1 + expect(await erc20Contract.methods.balanceOf(user2).call()).to.equal('0') + + + + }) + + it('#getCurrentTokens - should return current pool tokens', async () => { + const currentTokens = await pool.getCurrentTokens(poolAddress) + expect(currentTokens[0]).to.equal(erc20Token) + expect(currentTokens[1]).to.equal(contracts.daiAddress) + + }) + + +})