diff --git a/package-lock.json b/package-lock.json index 25e25936..b50b12fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3493,7 +3493,7 @@ } }, "@oceanprotocol/contracts": { - "version": "git+https://github.com/oceanprotocol/contracts.git#bb1382e700d6f2f994b8e3abfd6a7831e69b1804", + "version": "git+https://github.com/oceanprotocol/contracts.git#c1bea5033dfc9071105a11b63ce86d8e8f612b7b", "from": "git+https://github.com/oceanprotocol/contracts.git#v4main", "requires": { "@balancer-labs/v2-pool-utils": "^1.0.0", diff --git a/package.json b/package.json index ba716913..2fd3fdc5 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "changelog": "auto-changelog -p", "prepublishOnly": "npm run build", "test:ss": "mocha --config=test/unit/.mocharc.json --node-env=test --exit 'test/unit/pools/ssContracts/SideStaking.test.ts'", + "test:fixed": "mocha --config=test/unit/.mocharc.json --node-env=test --exit 'test/unit/pools/fixedRate/FixedRateExchange.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'", diff --git a/src/pools/fixedRate/FixedRateExchange.ts b/src/pools/fixedRate/FixedRateExchange.ts index 66928124..a87cfe79 100644 --- a/src/pools/fixedRate/FixedRateExchange.ts +++ b/src/pools/fixedRate/FixedRateExchange.ts @@ -1,6 +1,338 @@ +import defaultFixedRateExchangeABI from '@oceanprotocol/contracts/artifacts/contracts/pools/fixedRate/FixedRateExchange.sol/FixedRateExchange.json' +import defaultERC20ABI from '@oceanprotocol/contracts/artifacts/contracts/templates/ERC20Template.sol/ERC20Template.json' +import BigNumber from 'bignumber.js' +import { TransactionReceipt } from 'web3-core' +import { Contract, EventData } from 'web3-eth-contract' +import { AbiItem } from 'web3-utils/types' import Web3 from 'web3' +import { Logger, getFairGasPrice } from '../../utils' + +const MAX_AWAIT_PROMISES = 10 + +export interface FixedPriceExchange { + exchangeID?: string + exchangeOwner: string + dataToken: string + baseToken: string + fixedRate: string + active: boolean + supply: string +} + +export interface FixedPriceSwap { + exchangeID: string + caller: string + baseTokenAmount: string + dataTokenAmount: string +} + +export enum FixedRateCreateProgressStep { + CreatingExchange, + ApprovingDatatoken +} export class FixedRateExchange { public GASLIMIT_DEFAULT = 1000000 - public web3: Web3 = null -} + /** Ocean related functions */ + public oceanAddress: string = null + public fixedRateAddress:string + public fixedRateExchangeABI: AbiItem | AbiItem[] + public fixedRateContract:Contract + public web3: Web3 + public contract: Contract = null + private logger: Logger + + public startBlock: number + public ssABI: AbiItem | AbiItem[] + + + + /** + * Instantiate FixedRateExchange + * @param {any} web3 + * @param {any} fixedRateExchangeABI + */ + constructor( + web3: Web3, + logger: Logger, + fixedRateAddress: string, + fixedRateExchangeABI: AbiItem | AbiItem[] = null, + oceanAddress: string = null, + startBlock?: number + ) { + this.web3 = web3 + + if (startBlock) this.startBlock = startBlock + else this.startBlock = 0 + this.fixedRateExchangeABI = + fixedRateExchangeABI || (defaultFixedRateExchangeABI.abi as AbiItem[]) + this.oceanAddress = oceanAddress + this.fixedRateAddress = fixedRateAddress + this.contract = new this.web3.eth.Contract( + this.fixedRateExchangeABI, + this.fixedRateAddress + ) + + this.logger = logger + } + async amountToUnits(token: string, amount: string): Promise { + let decimals = 18 + const tokenContract = new this.web3.eth.Contract( + defaultERC20ABI.abi as AbiItem[], + token + ) + try { + decimals = await tokenContract.methods.decimals().call() + } catch (e) { + this.logger.error('ERROR: FAILED TO CALL DECIMALS(), USING 18') + } + + const amountFormatted = new BigNumber(parseInt(amount) * 10 ** decimals) + + return amountFormatted.toString() + } + + async unitsToAmount(token: string, amount: string): Promise { + let decimals = 18 + const tokenContract = new this.web3.eth.Contract( + defaultERC20ABI.abi as AbiItem[], + token + ) + try { + decimals = await tokenContract.methods.decimals().call() + } catch (e) { + this.logger.error('ERROR: FAILED TO CALL DECIMALS(), USING 18') + } + + const amountFormatted = new BigNumber(parseInt(amount) / 10 ** decimals) + + return amountFormatted.toString() + } + + + + /** + * Creates unique exchange identifier. + * @param {String} dataToken Data Token Contract Address + * @param {String} owner Owner of the exchange + * @return {Promise} exchangeId + */ + public async generateExchangeId(dataToken: string, owner: string): Promise { + const exchangeId = await this.contract.methods + .generateExchangeId(this.oceanAddress, dataToken, owner) + .call() + return exchangeId + } + + /** + * Atomic swap + * @param {String} exchangeId ExchangeId + * @param {Number} dataTokenAmount Amount of Data Tokens + * @param {String} address User address + * @return {Promise} transaction receipt + */ + public async buyDT( + exchangeId: string, + dataTokenAmount: string, + address: string + ): Promise { + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await this.contract.methods + .swap(exchangeId, this.web3.utils.toWei(String(dataTokenAmount))) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + try { + const trxReceipt = await this.contract.methods + .swap(exchangeId, this.web3.utils.toWei(String(dataTokenAmount))) + .send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + return trxReceipt + } catch (e) { + this.logger.error(`ERROR: Failed to buy datatokens: ${e.message}`) + return null + } + } + + /** + * Gets total number of exchanges + * @param {String} exchangeId ExchangeId + * @param {Number} dataTokenAmount Amount of Data Tokens + * @return {Promise} no of available exchanges + */ + public async getNumberOfExchanges(): Promise { + const numExchanges = await this.contract.methods.getNumberOfExchanges().call() + return numExchanges + } + + /** + * Set new rate + * @param {String} exchangeId ExchangeId + * @param {Number} newRate New rate + * @param {String} address User account + * @return {Promise} transaction receipt + */ + public async setRate( + exchangeId: string, + newRate: number, + address: string + ): Promise { + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await this.contract.methods + .setRate(exchangeId, this.web3.utils.toWei(String(newRate))) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + const trxReceipt = await this.contract.methods + .setRate(exchangeId, this.web3.utils.toWei(String(newRate))) + .send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + return trxReceipt + } + + /** + * Activate an exchange + * @param {String} exchangeId ExchangeId + * @param {String} address User address + * @return {Promise} transaction receipt + */ + public async activate( + exchangeId: string, + address: string + ): Promise { + const exchange = await this.getExchange(exchangeId) + if (!exchange) return null + if (exchange.active === true) return null + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await this.contract.methods + .toggleExchangeState(exchangeId) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + const trxReceipt = await this.contract.methods.toggleExchangeState(exchangeId).send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + return trxReceipt + } + + /** + * Deactivate an exchange + * @param {String} exchangeId ExchangeId + * @param {String} address User address + * @return {Promise} transaction receipt + */ + public async deactivate( + exchangeId: string, + address: string + ): Promise { + const exchange = await this.getExchange(exchangeId) + if (!exchange) return null + if (exchange.active === false) return null + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await this.contract.methods + .toggleExchangeState(exchangeId) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + const trxReceipt = await this.contract.methods.toggleExchangeState(exchangeId).send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + return trxReceipt + } + + /** + * Get Rate + * @param {String} exchangeId ExchangeId + * @return {Promise} Rate (converted from wei) + */ + public async getRate(exchangeId: string): Promise { + const weiRate = await this.contract.methods.getRate(exchangeId).call() + return this.web3.utils.fromWei(weiRate) + } + + /** + * Get Supply + * @param {String} exchangeId ExchangeId + * @return {Promise} Rate (converted from wei) + */ + public async getSupply(exchangeId: string): Promise { + const weiRate = await this.contract.methods.getSupply(exchangeId).call() + return this.web3.utils.fromWei(weiRate) + } + + /** + * getOceanNeeded + * @param {String} exchangeId ExchangeId + * @param {Number} dataTokenAmount Amount of Data Tokens + * @return {Promise} Ocean amount needed + */ + public async getOceanNeeded( + exchangeId: string, + dataTokenAmount: string + ): Promise { + const weiRate = await this.contract.methods + .CalcInGivenOut(exchangeId, this.web3.utils.toWei(dataTokenAmount)) + .call() + return this.web3.utils.fromWei(weiRate) + } + + /** + * Get exchange details + * @param {String} exchangeId ExchangeId + * @return {Promise} Exchange details + */ + public async getExchange(exchangeId: string): Promise { + const result: FixedPriceExchange = await this.contract.methods + .getExchange(exchangeId) + .call() + result.fixedRate = this.web3.utils.fromWei(result.fixedRate) + result.supply = this.web3.utils.fromWei(result.supply) + result.exchangeID = exchangeId + return result + } + + /** + * Get all exchanges + * @param {String} exchangeId ExchangeId + * @return {Promise} Exchanges list + */ + public async getExchanges(): Promise { + return await this.contract.methods.getExchanges().call() + } + + /** + * Check if an exchange is active + * @param {String} exchangeId ExchangeId + * @return {Promise} Result + */ + public async isActive(exchangeId: string): Promise { + const result = await this.contract.methods.isActive(exchangeId).call() + return result + } + + + + +} \ No newline at end of file diff --git a/test/unit/pools/fixedRate/FixedRateExchange.test.ts b/test/unit/pools/fixedRate/FixedRateExchange.test.ts new file mode 100644 index 00000000..eefdd174 --- /dev/null +++ b/test/unit/pools/fixedRate/FixedRateExchange.test.ts @@ -0,0 +1,161 @@ +import { assert, expect } from 'chai' +import { AbiItem } from 'web3-utils/types' +import { TestContractHandler } from '../../../TestContractHandler' +import { Contract } from 'web3-eth-contract' +import Web3 from 'web3' +import BigNumber from 'bignumber.js' +import ERC721Factory from '@oceanprotocol/contracts/artifacts/contracts/ERC721Factory.sol/ERC721Factory.json' +import ERC721Template from '@oceanprotocol/contracts/artifacts/contracts/templates/ERC721Template.sol/ERC721Template.json' +import SSContract 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 OPFCollector from '@oceanprotocol/contracts/artifacts/contracts/communityFee/OPFCommunityFeeCollector.sol/OPFCommunityFeeCollector.json' +import { LoggerInstance } from '../../../../src/utils' +import { NFTFactory } from '../../../../src/factories/NFTFactory' +import { Pool } from '../../../../src/pools/balancer/Pool' +import { FixedRateExchange } from '../../../../src/pools/fixedRate/FixedRateExchange' +const { keccak256 } = require('@ethersproject/keccak256') +const web3 = new Web3('http://127.0.0.1:8545') +const communityCollector = '0xeE9300b7961e0a01d9f0adb863C7A227A07AaD75' + +describe('Fixed Rate unit test', () => { + let factoryOwner: string + let nftOwner: string + let exchangeOwner: string + let user1: string + let user2: string + let user3: string + let initialBlock: number + let fixedRateAddress: string + let exchangeId: string + let contracts: TestContractHandler + let fixedRate: FixedRateExchange + let dtAddress: string + let dtAddress2: string + let dtContract: Contract + let daiContract: Contract + let usdcContract: Contract + const vestedBlocks = 2500000 + + 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[], + SSContract.abi as AbiItem[], + FixedRate.abi as AbiItem[], + Dispenser.abi as AbiItem[], + OPFCollector.abi as AbiItem[], + + ERC721Template.bytecode, + ERC20Template.bytecode, + PoolTemplate.bytecode, + ERC721Factory.bytecode, + FactoryRouter.bytecode, + SSContract.bytecode, + FixedRate.bytecode, + Dispenser.bytecode, + OPFCollector.bytecode + ) + await contracts.getAccounts() + factoryOwner = contracts.accounts[0] + nftOwner = contracts.accounts[1] + user1 = contracts.accounts[2] + user2 = contracts.accounts[3] + user3 = contracts.accounts[4] + exchangeOwner = contracts.accounts[0] + await contracts.deployContracts(factoryOwner, FactoryRouter.abi as AbiItem[]) + + // initialize fixed rate + // + + + daiContract = new web3.eth.Contract( + contracts.MockERC20.options.jsonInterface, + contracts.daiAddress + ) + + usdcContract = new web3.eth.Contract( + contracts.MockERC20.options.jsonInterface, + contracts.usdcAddress + ) + + + console.log( + await usdcContract.methods.decimals().call(), + 'USDC DECIMALS IN THIS TEST' + ) + + + }) + + describe('Test a Fixed Rate Exchange with DAI (18 Decimals)', () => { + it('#create an exchange', async () => { + // CREATE AN Exchange + // 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: [] + } + + //[baseToken,owner,marketFeeCollector,allowedSwapper] + const fixedRateData = { + fixedPriceAddress:contracts.fixedRateAddress, + addresses:[contracts.daiAddress,exchangeOwner,user3, '0x0000000000000000000000000000000000000000'], + uints:[18,18,web3.utils.toWei('1'),1e15,0] + } + + const nftFactory = new NFTFactory(contracts.factory721Address, web3, LoggerInstance) + + const txReceipt = await nftFactory.createNftErcWithFixedRate( + exchangeOwner, + nftData, + ercData, + fixedRateData + ) + + initialBlock = await web3.eth.getBlockNumber() + dtAddress = txReceipt.events.TokenCreated.returnValues.newTokenAddress + exchangeId = txReceipt.events.NewFixedRate.returnValues.exchangeId + + dtContract = new web3.eth.Contract(ERC20Template.abi as AbiItem[], dtAddress) + // user2 has no dt1 + expect(await dtContract.methods.balanceOf(user2).call()).to.equal('0') + + fixedRateAddress = contracts.fixedRateAddress + fixedRate = new FixedRateExchange(web3, LoggerInstance, fixedRateAddress, FixedRate.abi as AbiItem[],contracts.oceanAddress) + assert(fixedRate != null) + }) + + it('#isActive - should return true if exchange is active', async () => { + expect(await fixedRate.isActive(exchangeId)).to.equal( + true + ) + expect(await fixedRate.isActive('0x00')).to.equal( + false + ) + }) + + }) +})