diff --git a/package.json b/package.json index 93b9cb70..d4952dc6 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ }, "dependencies": { "@ethereum-navigator/navigator": "^0.5.0", - "@oceanprotocol/contracts": "^0.3.4", + "@oceanprotocol/contracts": "^0.3.5", "decimal.js": "^10.2.0", "fs": "0.0.1-security", "node-fetch": "^2.6.0", diff --git a/src/exchange/FixRateExchange.ts b/src/exchange/FixRateExchange.ts new file mode 100644 index 00000000..837a4e10 --- /dev/null +++ b/src/exchange/FixRateExchange.ts @@ -0,0 +1,233 @@ +const defaultFixedRateExchangeABI = require('@oceanprotocol/contracts/artifacts/FixedRateExchange.json') + +export interface FixedPricedExchange { + exchangeOwner: string + dataToken: string + baseToken: string + fixedRate: number + active: boolean +} + +export class OceanFixedRateExchange { + /** Ocean related functions */ + public oceanAddress: string = null + public fixedRateExchangeAddress: string + public fixedRateExchangeABI: any + public web3: any + public contract: any = null + + /** + * Instantiate FixedRateExchange + * @param {any} web3 + * @param {String} fixedRateExchangeAddress + * @param {any} fixedRateExchangeABI + * @param {String} oceanAddress + */ + constructor( + web3: any, + fixedRateExchangeAddress: string = null, + fixedRateExchangeABI: any = null, + oceanAddress: string = null + ) { + this.web3 = web3 + this.fixedRateExchangeAddress = fixedRateExchangeAddress + this.fixedRateExchangeABI = fixedRateExchangeABI || defaultFixedRateExchangeABI.abi + this.oceanAddress = oceanAddress + if (web3) + this.contract = new this.web3.eth.Contract( + this.fixedRateExchangeABI, + this.fixedRateExchangeAddress + ) + } + + /** + * Creates new exchange pair between Ocean Token and data token. + * @param {String} dataToken Data Token Contract Address + * @param {Number} rate exchange rate + * @param {String} address User address + * @return {Promise} exchangeId + */ + public async create(dataToken: string, rate: string, address: string): Promise { + const estGas = await this.contract.methods + .create(this.oceanAddress, dataToken, this.web3.utils.toWei(rate)) + .estimateGas(function (err, estGas) { + if (err) console.log('FixedPriceExchange: ' + err) + return estGas + }) + const trxReceipt = await this.contract.methods + .create(this.oceanAddress, dataToken, this.web3.utils.toWei(rate)) + .send({ + from: address, + gas: estGas + 1 + }) + + let exchangeId = null + try { + exchangeId = trxReceipt.events.ExchangeCreated.returnValues[0] + } catch (e) { + console.error(e) + } + return exchangeId + } + + /** + * 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 swap( + exchangeId: string, + dataTokenAmount: string, + address: string + ): Promise { + const estGas = await this.contract.methods + .swap(exchangeId, this.web3.utils.toWei(String(dataTokenAmount))) + .estimateGas(function (err, estGas) { + if (err) console.log('FixedPriceExchange: ' + err) + return estGas + }) + + const trxReceipt = await this.contract.methods + .swap(exchangeId, this.web3.utils.toWei(String(dataTokenAmount))) + .send({ + from: address, + gas: estGas + 1 + }) + return trxReceipt + } + + /** + * 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 estGas = await this.contract.methods + .setRate(exchangeId, this.web3.utils.toWei(String(newRate))) + .estimateGas(function (err, estGas) { + if (err) console.log('FixedPriceExchange: ' + err) + return estGas + }) + const trxReceipt = await this.contract.methods + .setRate(exchangeId, this.web3.utils.toWei(String(newRate))) + .send({ + from: address, + gas: estGas + 1 + }) + 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 estGas = await this.contract.methods + .activate(exchangeId) + .estimateGas(function (err, estGas) { + if (err) console.log('FixedPriceExchange: ' + err) + return estGas + }) + const trxReceipt = await this.contract.methods.activate(exchangeId).send({ + from: address, + gas: estGas + 1 + }) + 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 estGas = await this.contract.methods + .deactivate(exchangeId) + .estimateGas(function (err, estGas) { + if (err) console.log('FixedPriceExchange: ' + err) + return estGas + }) + const trxReceipt = await this.contract.methods.deactivate(exchangeId).send({ + from: address, + gas: estGas + 1 + }) + 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 exchange details + * @param {String} exchangeId ExchangeId + * @return {Promise} Exchange details + */ + public async getExchange(exchangeId: string): Promise { + const result: FixedPricedExchange = await this.contract.methods + .getExchange(exchangeId) + .call() + return result + } + + /** + * Get all exchanges + * @param {String} exchangeId ExchangeId + * @return {Promise} Exchanges list + */ + public async getExchanges(): Promise { + const result = await this.contract.methods.getExchanges().call() + return result + } + + /** + * 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 + } +} diff --git a/src/models/Config.ts b/src/models/Config.ts index bc6bdfdf..cd40bd43 100644 --- a/src/models/Config.ts +++ b/src/models/Config.ts @@ -68,12 +68,24 @@ export class Config { */ public poolFactoryABI?: any + /** * Pool ABI * @type {string} */ public poolABI?: any + /** + * FixedRateExchangeAddress + * @type {string} + */ + public fixedRateExchangeAddress?: string + + /** + * FixedRateExchangeAddressABI + * @type {any} + */ + public fixedRateExchangeAddressABI?: any /** * Log level. * @type {boolean | LogLevel} diff --git a/src/ocean/Ocean.ts b/src/ocean/Ocean.ts index 78e0aa8b..eef02a8d 100644 --- a/src/ocean/Ocean.ts +++ b/src/ocean/Ocean.ts @@ -13,6 +13,7 @@ import { } from '../Instantiable.abstract' import { Compute } from './Compute' import { OceanPool } from '../balancer/OceanPool' +import {OceanFixedRateExchange, FixedPricedExchange} from '../exchange/FixRateExchange' /** * Main interface for Ocean Protocol. @@ -57,6 +58,13 @@ export class Ocean extends Instantiable { instanceConfig.config.poolFactoryAddress, instanceConfig.config.oceanTokenAddress ) + instance.fixedRateExchange = new OceanFixedRateExchange( + instanceConfig.config.web3Provider, + instanceConfig.config.fixedRateExchangeAddress, + instanceConfig.config.fixedRateExchangeAddressABI, + instanceConfig.config.oceanTokenAddress + + ) instance.versions = await Versions.getInstance(instanceConfig) instance.network = new Network() return instance @@ -123,6 +131,12 @@ export class Ocean extends Instantiable { */ public pool: OceanPool + /** + * Ocean FixedRateExchange submodule + * @type {OceanFixedRateExchange} + */ + public fixedRateExchange: OceanFixedRateExchange + /** * Ocean tokens submodule * @type {OceanTokens} diff --git a/src/utils/ConfigHelper.ts b/src/utils/ConfigHelper.ts index c7fb6e06..b7954579 100644 --- a/src/utils/ConfigHelper.ts +++ b/src/utils/ConfigHelper.ts @@ -19,7 +19,8 @@ const configs = [ factoryAddress: null, metadataStoreUri: 'http://127.0.0.1:5000', providerUri: 'http://127.0.0.1:8030', - poolFactoryAddress: null + poolFactoryAddress: null, + fixedRateExchangeAddress: null }, { chainId: 4, @@ -29,7 +30,8 @@ const configs = [ oceanTokenAddress: '0x8967BCF84170c91B0d24D4302C2376283b0B3a07', metadataStoreUri: 'https://aquarius.rinkeby.v3.dev-ocean.com', providerUri: 'https://provider.rinkeby.v3.dev-ocean.com', - poolFactoryAddress: '0xA4531C624A3D88323a1e178DABe1233AF178701B' + poolFactoryAddress: '0xA4531C624A3D88323a1e178DABe1233AF178701B', + fixedRateExchangeAddress: '0x7219AfFc1C2b474830D9d9b0423ecf47073C5488' }, { chainId: 1, @@ -39,7 +41,8 @@ const configs = [ oceanTokenAddress: '0x985dd3d42de1e256d09e1c10f112bccb8015ad41', metadataStoreUri: null, providerUri: null, - poolFactoryAddress: null + poolFactoryAddress: null, + fixedRateExchangeAddress: null } ] diff --git a/test/FixedPriceContractHandler.ts b/test/FixedPriceContractHandler.ts new file mode 100644 index 00000000..ea131bd4 --- /dev/null +++ b/test/FixedPriceContractHandler.ts @@ -0,0 +1,58 @@ +import Web3 from 'web3' +import { Contract } from 'web3-eth-contract' +import { AbiItem } from 'web3-utils/types' + +export class FixedPricedContractHandler { + public contract: Contract + public accounts: string[] + public contractBytecode: string + public contractAddress: string + public web3: Web3 + + constructor( + contractABI: AbiItem | AbiItem[], + contractBytecode: string, + web3: Web3 + ) { + this.web3 = web3 + this.contract = new this.web3.eth.Contract(contractABI) + this.contractBytecode = contractBytecode + + } + + public async getAccounts() { + this.accounts = await this.web3.eth.getAccounts() + } + + public async deployContracts() { + let estGas + + await this.getAccounts() + // get est gascost + estGas = await this.contract + .deploy({ + data: this.contractBytecode, + arguments: [] + }) + .estimateGas(function (err, estGas) { + if (err) console.log('DeployContracts: ' + err) + return estGas + }) + // deploy the contract and get it's address + this.contractAddress = await this.contract + .deploy({ + data: this.contractBytecode, + arguments: [] + }) + .send({ + from: this.accounts[0], + gas: estGas + 1, + gasPrice: '3000000000' + }) + .then(function (contract) { + return contract.options.address + }) + + + } +} diff --git a/test/unit/exchanges/FixedPriceExchange.test.ts b/test/unit/exchanges/FixedPriceExchange.test.ts new file mode 100644 index 00000000..5acdf021 --- /dev/null +++ b/test/unit/exchanges/FixedPriceExchange.test.ts @@ -0,0 +1,293 @@ +import { assert } from 'chai' +import { AbiItem } from 'web3-utils/types' +import { TestContractHandler } from '../../TestContractHandler' +import { FixedPricedContractHandler } from '../../FixedPriceContractHandler' +import { DataTokens } from '../../../src/datatokens/Datatokens' +import { + OceanFixedRateExchange, + FixedPricedExchange +} from '../../../src/exchange/FixRateExchange' + +import Web3 from 'web3' +import factory from '@oceanprotocol/contracts/artifacts/DTFactory.json' +import datatokensTemplate from '@oceanprotocol/contracts/artifacts/DataTokenTemplate.json' +import FixedRateExchangeContract = require('@oceanprotocol/contracts/artifacts/FixedRateExchange.json') + +import BigNumber from 'bignumber.js' +const web3 = new Web3('http://127.0.0.1:8545') + +describe('FixedRateExchange flow', () => { + let oceanTokenAddress + let FixedRateExchangeAddress + let FixedRateClass + let oceandatatoken + let aliceExchangeId + let bob + let alice + let datatoken + let tokenAddress + + let alicePoolAddress + let currentDtPrice + let owner + let contracts + + let consoleDebug: false + let greatPool + const tokenAmount = '1000' + const transferAmount = '200' + const blob = 'http://localhost:8030/api/v1/services/consume' + describe('#test', () => { + before(async () => { + // deploy SFactory + const Contracts = new FixedPricedContractHandler( + FixedRateExchangeContract.abi as AbiItem[], + FixedRateExchangeContract.bytecode, + web3 + ) + await Contracts.getAccounts() + owner = Contracts.accounts[0] + + await Contracts.deployContracts() + FixedRateExchangeAddress = Contracts.contractAddress + assert(FixedRateExchangeAddress !== null) + + // deploy DT Factory + contracts = new TestContractHandler( + factory.abi as AbiItem[], + datatokensTemplate.abi as AbiItem[], + datatokensTemplate.bytecode, + factory.bytecode, + web3 + ) + await contracts.getAccounts() + owner = contracts.accounts[0] + alice = contracts.accounts[1] + bob = contracts.accounts[2] + await contracts.deployContracts(owner) + + // initialize DataTokens + datatoken = new DataTokens( + contracts.factoryAddress, + factory.abi as AbiItem[], + datatokensTemplate.abi as AbiItem[], + web3 + ) + assert(datatoken !== null) + }) + + it('should create datatokens smart contract', async () => { + tokenAddress = await datatoken.create(blob, alice) + assert(tokenAddress !== null) + console.log('data Token address:' + tokenAddress) + }) + it('Create a dummy OceanToken', async () => { + // Bob creates a Datatoken + oceandatatoken = new DataTokens( + contracts.factoryAddress, + factory.abi as AbiItem[], + datatokensTemplate.abi as AbiItem[], + web3 + ) + oceanTokenAddress = await oceandatatoken.create(blob, bob) + console.log('oceanTokenAddress:' + oceanTokenAddress) + }) + + it('should initialize FixedExchangeRate class', async () => { + FixedRateClass = new OceanFixedRateExchange( + web3, + FixedRateExchangeAddress, + FixedRateExchangeContract.abi, + oceanTokenAddress + ) + assert(FixedRateClass !== null) + }) + + it('Alice mints 1000 tokens', async () => { + await datatoken.mint(tokenAddress, alice, tokenAmount) + }) + it('Bob mints 1000 Ocean tokens', async () => { + await oceandatatoken.mint(oceanTokenAddress, bob, tokenAmount) + }) + it('Alice should have 1000 tokens', async () => { + const balance = await datatoken.balance(tokenAddress, alice) + console.log(balance) + }) + it('Bob should have 1000 ocean tokens', async () => { + const balance = await oceandatatoken.balance(oceanTokenAddress, bob) + console.log(balance) + }) + it('Alice creates a new FixedRate Exchange with a rate of 0.5', async () => { + aliceExchangeId = await FixedRateClass.create(tokenAddress, '0.1', alice) + console.log(aliceExchangeId) + }) + it('Alice allows Exchange to spend 1000 data tokens', async () => { + await datatoken.approve(tokenAddress, FixedRateExchangeAddress, tokenAmount, alice) + }) + it('Bob allows Exchange to spend 1000 ocean tokens', async () => { + await oceandatatoken.approve( + oceanTokenAddress, + FixedRateExchangeAddress, + tokenAmount, + bob + ) + }) + it('Alice should aproved speding datatokens', async () => { + const balance = await datatoken.allowance( + tokenAddress, + alice, + FixedRateExchangeAddress + ) + console.log(balance) + }) + it('Bob should aproved speding oceantokens', async () => { + const balance = await oceandatatoken.allowance( + oceanTokenAddress, + bob, + FixedRateExchangeAddress + ) + console.log(balance) + }) + it('Bob should get the exchange details', async () => { + const exchangeDetails = await FixedRateClass.getExchange(aliceExchangeId) + console.log(exchangeDetails) + }) + it('Bob should swap 1 DataToken', async () => { + const swapResult = await FixedRateClass.swap(aliceExchangeId, '1', bob) + console.log(swapResult) + }) + /* + it('Alice creates a new OceanPool pool', async () => { + /// new pool with total DT = 45 , dt weight=90% with swap fee 2% + alicePoolAddress = await Pool.createDTPool(alice, tokenAddress, 45, 9, '0.02') + }) + it('Get pool information', async () => { + const currentTokens = await Pool.getCurrentTokens(alice, alicePoolAddress) + assert(currentTokens.length === 2) + assert(currentTokens.includes(tokenAddress)) + assert(currentTokens.includes(oceanTokenAddress)) + }) + it('Get pool swap fee', async () => { + const currentSwapFee = await Pool.getSwapFee(alice, alicePoolAddress) + assert(currentSwapFee === '0.02') + }) + it('Get dtPrice from the pool ', async () => { + currentDtPrice = await Pool.getDTPrice(alice, alicePoolAddress) + assert(currentDtPrice > 0) + }) + it('Get dtToken pool reserve ', async () => { + const currentDtReserve = await Pool.getDTReserve(alice, alicePoolAddress) + assert(currentDtReserve > 0) + }) + it('Get Ocean pool reserve ', async () => { + const currentOceanReserve = await Pool.getOceanReserve(alice, alicePoolAddress) + assert(currentOceanReserve > 0) + }) + it('Get total supply of pool tokens', async () => { + const totalSupply = await Pool.totalSupply(alicePoolAddress) + assert(totalSupply > 0) + }) + it('Get amount of Ocean needed to buy 1 dtToken', async () => { + const requiredOcean = await Pool.getOceanNeeded(alice, alicePoolAddress, '1') + assert(requiredOcean > 0) + }) + + it('Bob should search for pools with this DT', async () => { + const pools = await Pool.searchPoolforDT(bob, tokenAddress) + assert(pools.length > 0) + greatPool = pools[0] + }) + it('Bob should buy a DT ', async () => { + const maxPrice = parseFloat(currentDtPrice) * 2 + await Pool.buyDT(bob, greatPool, '1', '2', String(maxPrice)) + const bobDtBalance = await datatoken.balance(tokenAddress, bob) + const bobOceanBalance = await datatoken.balance(oceanTokenAddress, bob) + assert(bobDtBalance > 0) + assert(bobOceanBalance > 0) + }) + it('Bob should add DT liquidity to pool ', async () => { + const currentDtReserve = await Pool.getDTReserve(bob, greatPool) + if (consoleDebug) console.log('currentDtReserve:' + currentDtReserve) + const bobDtBalance = await datatoken.balance(tokenAddress, bob) + if (consoleDebug) console.log('BOB DT Balance:' + bobDtBalance) + await Pool.addDTLiquidity(bob, greatPool, bobDtBalance) + + const newbobDtBalance = await datatoken.balance(tokenAddress, bob) + + const newDtReserve = await Pool.getDTReserve(bob, greatPool) + + const sharesBalance = await Pool.sharesBalance(bob, greatPool) + if (consoleDebug) console.log('newDtReserve:' + newDtReserve) + if (consoleDebug) console.log('newbobDtBalance:' + newbobDtBalance) + if (consoleDebug) console.log('sharesBalance:' + sharesBalance) + assert(parseFloat(newbobDtBalance) < parseFloat(bobDtBalance)) + assert(parseFloat(newDtReserve) > parseFloat(currentDtReserve)) + assert(parseFloat(sharesBalance) > 0) + }) + + it('Bob should remove DT liquidity from pool ', async () => { + const currentDtReserve = await Pool.getDTReserve(bob, greatPool) + if (consoleDebug) console.log('currentDtReserve:' + currentDtReserve) + const bobDtBalance = await datatoken.balance(tokenAddress, bob) + if (consoleDebug) console.log('bobDtBalance:' + bobDtBalance) + const poolShares = await Pool.sharesBalance(bob, greatPool) + if (consoleDebug) console.log('poolShares:' + poolShares) + await Pool.removeDTLiquidity(bob, greatPool, '0.75', poolShares) + + const newDtReserve = await Pool.getDTReserve(bob, greatPool) + if (consoleDebug) console.log('newDtReserve:' + newDtReserve) + const newbobDtBalance = await datatoken.balance(tokenAddress, bob) + if (consoleDebug) console.log('newbobDtBalance:' + newbobDtBalance) + const newpoolShares = await Pool.sharesBalance(bob, greatPool) + if (consoleDebug) console.log('newpoolShares:' + newpoolShares) + assert(parseFloat(newDtReserve) < parseFloat(currentDtReserve)) + assert(parseFloat(bobDtBalance) < parseFloat(newbobDtBalance)) + assert(parseFloat(poolShares) > parseFloat(newpoolShares)) + }) + + it('Bob should add Ocean liquidity to pool ', async () => { + const currentDtReserve = await Pool.getOceanReserve(bob, greatPool) + const bobDtBalance = await datatoken.balance(oceanTokenAddress, bob) + if (consoleDebug) console.log('currentDtReserve:' + currentDtReserve) + if (consoleDebug) console.log('bobDtBalance:' + bobDtBalance) + + await Pool.addOceanLiquidity(bob, greatPool, '1') + + const newbobDtBalance = await datatoken.balance(oceanTokenAddress, bob) + + const newDtReserve = await Pool.getOceanReserve(bob, greatPool) + + const sharesBalance = await Pool.sharesBalance(bob, greatPool) + if (consoleDebug) console.log('newDtReserve:' + newDtReserve) + if (consoleDebug) console.log('newbobDtBalance:' + newbobDtBalance) + if (consoleDebug) console.log('sharesBalance:' + sharesBalance) + assert(parseFloat(newbobDtBalance) < parseFloat(bobDtBalance)) + assert(parseFloat(newDtReserve) > parseFloat(currentDtReserve)) + assert(parseFloat(sharesBalance) > 0) + }) + + it('Bob should remove Ocean liquidity from pool ', async () => { + const currentDtReserve = await Pool.getOceanReserve(bob, greatPool) + const bobDtBalance = await datatoken.balance(oceanTokenAddress, bob) + + const poolShares = await Pool.sharesBalance(bob, greatPool) + if (consoleDebug) console.log('currentDtReserve:' + currentDtReserve) + if (consoleDebug) console.log('bobDtBalance:' + bobDtBalance) + if (consoleDebug) console.log('poolShares:' + poolShares) + + await Pool.removeOceanLiquidity(bob, greatPool, '0.75', poolShares) + + const newDtReserve = await Pool.getOceanReserve(bob, greatPool) + const newbobDtBalance = await datatoken.balance(oceanTokenAddress, bob) + const newpoolShares = await Pool.sharesBalance(bob, greatPool) + + if (consoleDebug) console.log('newDtReserve:' + newDtReserve) + if (consoleDebug) console.log('newbobDtBalance:' + newbobDtBalance) + if (consoleDebug) console.log('newpoolShares:' + newpoolShares) + assert(parseFloat(newDtReserve) < parseFloat(currentDtReserve)) + assert(parseFloat(bobDtBalance) < parseFloat(newbobDtBalance)) + assert(parseFloat(poolShares) > parseFloat(newpoolShares)) + }) + */ + }) +})