diff --git a/package.json b/package.json index dc9d474..3e8ca18 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,18 @@ "lint": "vue-cli-service lint" }, "dependencies": { + "@ocrv/vue-tailwind-modal": "^1.0.0", + "@walletconnect/web3-provider": "^1.6.6", + "audit": "^0.0.6", "core-js": "^3.6.5", + "ethers": "^5.5.1", "vue": "^3.0.0", - "vue-router": "^4.0.0-0", - "vuex": "^3.6.2" + "vue-router": "^4.0.12", + "vue-spinner": "^1.0.4", + "vuex": "^4.0.2", + "web3": "^1.6.1", + "web3-utils": "^1.6.1", + "web3modal": "^1.9.2" }, "devDependencies": { "@vue/cli-plugin-babel": "~4.5.0", diff --git a/src/App.css b/src/App.css index 76309aa..7683b5f 100644 --- a/src/App.css +++ b/src/App.css @@ -1,3 +1,7 @@ .Card-Color { background-color: #ffedd1!important } +.jYxAGf { + margin-left: 0!important; + left: 0px!important; +} diff --git a/src/App.vue b/src/App.vue index bbd03a8..c8b779b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,6 +1,8 @@ + + + diff --git a/src/components/global/ConnectButton.vue b/src/components/global/ConnectButton.vue index 858038c..1638bcd 100644 --- a/src/components/global/ConnectButton.vue +++ b/src/components/global/ConnectButton.vue @@ -1,21 +1,47 @@ diff --git a/src/components/global/WalletBanance.vue b/src/components/global/WalletBanance.vue new file mode 100644 index 0000000..99005a1 --- /dev/null +++ b/src/components/global/WalletBanance.vue @@ -0,0 +1,42 @@ + + + + + + diff --git a/src/components/section/EthereumSource.vue b/src/components/section/EthereumSource.vue index 6d4dae5..61ca448 100644 --- a/src/components/section/EthereumSource.vue +++ b/src/components/section/EthereumSource.vue @@ -1,43 +1,106 @@ diff --git a/src/components/section/Navbar.vue b/src/components/section/Navbar.vue index cc03c70..776caa6 100644 --- a/src/components/section/Navbar.vue +++ b/src/components/section/Navbar.vue @@ -1,26 +1,86 @@ diff --git a/src/components/section/Swap.vue b/src/components/section/Swap.vue index f3eb149..4126bfc 100644 --- a/src/components/section/Swap.vue +++ b/src/components/section/Swap.vue @@ -1,6 +1,6 @@ diff --git a/src/components/section/WalletState.vue b/src/components/section/WalletState.vue index f1fa868..dc85542 100644 --- a/src/components/section/WalletState.vue +++ b/src/components/section/WalletState.vue @@ -1,13 +1,13 @@ diff --git a/src/config/mixins.js b/src/config/mixins.js new file mode 100644 index 0000000..3b7c39f --- /dev/null +++ b/src/config/mixins.js @@ -0,0 +1,54 @@ +import {mapGetters, mapState} from "vuex"; +import {getMultiplier} from "@/utils/helpers"; + +const web3Modal = { + computed: { + ...mapState(['web3Modal']), + ...mapGetters(['predictionsContract']) + }, + active() { + return this.web3Modal.active + } +} + +const predictionMixin = { + computed: { + ...mapState(['prediction']), + currentEpoch() { + return this.prediction.market.epoch + }, + bullMultiplier() { + return getMultiplier(this.round.totalAmount, this.round.bullAmount) + }, + bearMultiplier() { + return getMultiplier(this.round.totalAmount, this.round.bearAmount) + }, + bet() { + const {account} = this.$store.state.web3Modal + const {bets} = this.prediction + const roundId = this.round.id + + if (!bets[account]) { + return null + } + if (!bets[account][roundId]) { + return null + } + return this.prediction.bets[account][roundId] + }, + hasEntered() { + return this.bet !== null + }, + hasEnteredUp() { + return this.hasEntered && this.bet.position === "Bull" + }, + hasEnteredDown() { + return this.hasEntered && this.bet.position === "Bear" + } + } +} + +export { + web3Modal, + predictionMixin +} diff --git a/src/data/mock/mockDataProvider.js b/src/data/mock/mockDataProvider.js new file mode 100644 index 0000000..cbc7b3f --- /dev/null +++ b/src/data/mock/mockDataProvider.js @@ -0,0 +1,143 @@ +const faker = require('faker/locale/en') + +export const proposals = [ + { + id: '0x109b588a4f2a234e302c722f91fe42c5ab828a32', + org: { + title: "2214 N 7th St, Saint Joseph, MO 64505, United States" + }, + createdBy: '0x220866b1a2219f40e72f5c628b65d54268ca3a9d', + title: faker.commerce.productName(), + description: faker.lorem.paragraphs(6), + startTimestamp: 1626796800, + endTimestamp: 1627228800, + options: [ + { + id: "0x04816db5c52241596376a07b0ed6306c8eef74ac7f9c3767ca11e2088c3a8f52", + title: "Option A" + }, + { + id: "0x3d028447bfe1dcc2c859f717b9d62b86e956e0df786c46818f8bb8479ba4a710", + title: "Option B" + }, + { + id: "0x0b0a497079ec1c7246ec20851a38df9f87eed1d94ffe497288f48166d01e7762", + title: "Option C" + } + ] + }, +] + +export const marketOffers = () => { + var offers = [] + + for (var i = 0; i < faker.datatype.number(10) + 2; i++) { + let offer = { + id: faker.datatype.hexaDecimal(40), + world: { + env: { + currency: { + code: "USD", + symbol: "$", + rate: { + eth: 0.00046, + }, + }, + measurements: { + area: { + system: 'imperial', + unit: 'sqft', + }, + } + }, + property: { + address: `${ faker.address.streetAddress(true) }, ${ faker.address.cityName() }, ${ faker.address.stateAbbr() }, ${ faker.address.zipCode() }, ${ faker.address.country() }`, + currentRent: faker.datatype.number(8000) + 800, + marketValue: faker.datatype.number(90000000) + 200000, + area: faker.datatype.number(3000) + 1000, + rooms: { + bd: faker.datatype.number(6), + ba: faker.datatype.number(6), + }, + grossYieldPct: faker.datatype.number(12), + yearBuilt: faker.datatype.number(20) + 1990, + coverImageUrl: faker.image.imageUrl(), + }, + }, + chain: { + holderCount: faker.datatype.number(30000), + erc20: { + address: "0xfdf21d1cd5d3f0edbaed7cd1172ab3e49882d056", + code: "FRBA-" + faker.datatype.hexaDecimal(40), + marketCap: faker.datatype.number(90000000) + 200000, + price: 0.00028, + }, + }, + } + + offers.push(offer) + } + + return offers +} + +export const myAssets = () => { + var assets = [] + + for (var i = 0; i < faker.datatype.number(0) + 1; i++) { + let asset = { + id: faker.datatype.hexaDecimal(40), + world: { + env: { + currency: { + code: "USD", + symbol: "$", + rate: { + eth: 0.00046, + }, + }, + measurements: { + area: { + system: 'imperial', + unit: 'sqft', + }, + } + }, + property: { + address: `${ faker.address.streetAddress(true) }, ${ faker.address.cityName() }, ${ faker.address.stateAbbr() }, ${ faker.address.zipCode() }, ${ faker.address.country() }`, + currentRent: faker.datatype.number(8000) + 800, + marketValue: faker.datatype.number(90000000) + 200000, + area: faker.datatype.number(3000) + 1000, + rooms: { + bd: faker.datatype.number(6), + ba: faker.datatype.number(6), + }, + grossYieldPct: faker.datatype.number(12), + yearBuilt: faker.datatype.number(20) + 1990, + coverImageUrl: faker.image.imageUrl(), + }, + }, + chain: { + holderCount: faker.datatype.number(30000), + erc20: { + address: "0xfdf21d1cd5d3f0edbaed7cd1172ab3e49882d056", + code: "FRBA-" + faker.datatype.hexaDecimal(40), + marketCap: faker.datatype.number(90000000) + 200000, + price: 0.00028, + balance: 44772.22, + }, + dao: { + proposals: [ + proposals[0], + ], + proposalsOpen: 1, + votingStrengthFactor: 2, + }, + }, + } + + assets.push(asset) + } + + return assets +} \ No newline at end of file diff --git a/src/data/network/storage/ipfs/IPFSStorageNetwork.js b/src/data/network/storage/ipfs/IPFSStorageNetwork.js new file mode 100644 index 0000000..2b688f4 --- /dev/null +++ b/src/data/network/storage/ipfs/IPFSStorageNetwork.js @@ -0,0 +1,83 @@ +const network = require('../../../../utils/network') +const { create } = require('ipfs-http-client') +import StorageNetwork from '../storageNetwork' + +const ipfsAPIClient = create('https://ipfs.infura.io:5001/api/v0') + +class IPFSStorageNetwork extends StorageNetwork { + constructor() { + super() + } + + async addFile(file) { + let jsonString = JSON.stringify(file, null, 2) + return await ipfsAPIClient.add(jsonString, { pin: true }) + } + + getFile = ( + name + ) => new Promise((resolve, reject) => { + const url = `https://ipfs.infura.io:5001/api/v0/cat` + + let params = { + arg: name + } + + let headers = { } + //headers['Authorization'] = `Basic ${auth}` + + let data = { } + network + .postRequest( + url, + params, + headers, + data + ) + .then((res) => { + resolve(res) + }) + .catch((err) => { + reject(err) + }) + }) + + async getFiles(names) { + console.log('Requesting files from IPFS') + + const url = `https://ipfs.infura.io:5001/api/v0/cat` + + let headers = { } + //headers['Authorization'] = `Basic ${auth}` + + let data = { } + + const requests = names.map(async name => { + let params = { + arg: name + } + + return new Promise((resolve) => { + network + .postRequest( + url, + params, + headers, + data + ) + .then(response => { + resolve(response) + }) + .catch((thrown) => { + resolve(null) + }) + }) + }) + + const responses = (await Promise.all(requests)).filter(Boolean) + + return responses + } +} + +export default IPFSStorageNetwork \ No newline at end of file diff --git a/src/data/network/storage/storageNetwork.js b/src/data/network/storage/storageNetwork.js new file mode 100644 index 0000000..8a6c7ca --- /dev/null +++ b/src/data/network/storage/storageNetwork.js @@ -0,0 +1,9 @@ +class StorageNetwork { + constructor() { } + + async addFile(file) { } + getFile = (name) => new Promise((resolve, reject) => { }) + async getFiles(names) { } +} + +export default StorageNetwork \ No newline at end of file diff --git a/src/data/network/web3/contracts/assetContract.js b/src/data/network/web3/contracts/assetContract.js new file mode 100644 index 0000000..351001c --- /dev/null +++ b/src/data/network/web3/contracts/assetContract.js @@ -0,0 +1,141 @@ +/* global BigInt */ +// const { ethers } = require("ethers") + +// import EthereumClient from '../ethereum/ethereumClient' + +import contractAbi from "./abi/Asset.json" +const contractAddress = "0xe820eC01a88752d4b751327ceadD1cE9ACa32697" + +// const contractAbi = [ +// // Make a buy order +// "function buy(uint256 amount, uint256 price) payable", + +// // Create a standard proposal +// "function proposePaper(string info) returns (uint256)", + +// // Vote Yes on a certain proposal +// "function voteYes(uint256 id)", + +// // Vote No on a certain proposal +// "function voteNo(uint256 id)", + +// // Event that is triggered every time an order is filled on the market +// "event Filled(address indexed sender, address indexed recipient, uint256 indexed price, uint256 amount)" +// ] +// const startBlock = 0 // TODO: Inject the actual contract deployment block instead + +/** + * Asset contract + * @param {EthereumClient} ethereumClient Ethereum client + */ +class AssetContract { + constructor( + ethereumClient ) { + this.contract = ethereumClient.getContract(contractAddress, contractAbi) + this.mutableContract = ethereumClient.getMutableContract(this.contract) + // this.getBalanceTokens() + console.log(this.contract, this.mutableContract, '------contract') + } + + async getBalanceTokens() { + // const balance = (await this.contract.balanceOf((await this.provider.getSigners())[0].address)).toString(); + } + + /** + * Create a standard proposal + * @param {string} info Proposal info + */ + async proposePaper( + info + ) { + console.log('Creating a proposal..') + + let tx = await this.mutableContract + .proposePaper( + info, + { + gasLimit: 5000000 + } + ) + + return (await tx.wait()).status + } + + /** + * Make a buy order + * @param {number} amount Amount of shares to buy + */ + async sendTokens(amount) { + console.log(amount) + let tx = await this.contract.lockToken(amount) + console.log(tx, '------tx') + // return (await tx.wait()).status + } + + /** + * Make a buy order + * @param {number} amount Amount of shares to buy + * @param {BigInt} price Price to buy at + */ + async buy( + amount, + price + ) { + console.log('Amount ' + amount) + console.log('Price ' + price) + + let tx = await this.mutableContract + .buy( + amount, + price, + { + value: BigInt(amount) * BigInt(price), + gasLimit: 5000000 + } + ) + + return (await tx.wait()).status + } + + /** + * Vote Yes on a certain proposal + * @param {string} proposalId ID of the proposal + */ + async voteYes( + proposalId + ) { + console.log('Voting Yes on the proposal ' + proposalId) + + let tx = await this.mutableContract + .voteYes( + proposalId, + { + gasLimit: 5000000 + } + ) + + return (await tx.wait()).status + } + + /** + * Vote No on a certain proposal + * @param {string} proposalId ID of the proposal + */ + async voteNo( + proposalId + ) { + console.log('Voting No on the proposal ' + proposalId) + + let tx = await this.mutableContract + .voteNo( + proposalId, + { + gasLimit: 5000000 + } + ) + + return (await tx.wait()).status + } +} + +export default AssetContract diff --git a/src/data/network/web3/contracts/platformContract.js b/src/data/network/web3/contracts/platformContract.js new file mode 100644 index 0000000..9b39db8 --- /dev/null +++ b/src/data/network/web3/contracts/platformContract.js @@ -0,0 +1,22 @@ +import EthereumClient from '../ethereum/ethereumClient' + +const contractAddress = "0x0" // TODO: ADD CONTRACT ADDRESS +const contractAbi = [ + +] +const startBlock = 0 // TODO: ADD CONTRACT DEPLOYMENT BLOCK TO AVOID QUERYING EXTRA HISTORY + +/** + * Platform contract + * @param {EthereumClient} ethereumClient Ethereum client + */ +class PlatformContract { + constructor( + ethereumClient + ) { + this.contract = ethereumClient.getContract(contractAddress, contractAbi) + this.mutableContract = ethereumClient.getMutableContract(this.contract) + } +} + +export default PlatformContract diff --git a/src/data/network/web3/ethereum/abi/Asset.json b/src/data/network/web3/ethereum/abi/Asset.json new file mode 100644 index 0000000..82f8e12 --- /dev/null +++ b/src/data/network/web3/ethereum/abi/Asset.json @@ -0,0 +1,91 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_finuTokenContractAddress", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "lockToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_finuTokenContractAddress", + "type": "address" + } + ], + "name": "setFinuTokenContractAddress", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/src/data/network/web3/ethereum/ethereumClient.js b/src/data/network/web3/ethereum/ethereumClient.js new file mode 100644 index 0000000..7e9d9b3 --- /dev/null +++ b/src/data/network/web3/ethereum/ethereumClient.js @@ -0,0 +1,169 @@ +require('dotenv').config() +const { ethers } = require("ethers") +import Web3Modal from "web3modal" +import WalletConnectProvider from "@walletconnect/web3-provider" +import contractAbi from "./abi/Asset.json" + +const contractAddress = "0xA076c6bB4b1c5dED48488CAeB5b1799DE8dEFaD7" +const tokenAddress = "0xe820eC01a88752d4b751327ceadD1cE9ACa32697" + +/** + * @property {ethers.JsonRpcSigner} walletProvider + * @property {ethers.JsonRpcSigner} walletSigner + * @property {Web3Modal} web3Modal + * @property {FinuContract} fContract + * @property {FinuContract} tokenContract + * @property provider + */ +class EthereumClient { + constructor() { + const providerOptions = { + walletconnect: { + package: WalletConnectProvider, + options: { + infuraId: "8043bb2cf99347b1bfadfb233c5325c0" + } + } + } + this.web3Modal = new Web3Modal({ + cacheProvider: false, + providerOptions + }) + } + + /* --- Blockchain state --- */ + + /** + * Get current block number. + */ + async getBlockNumber() { + const number = await this.readProvider.getBlockNumber() + console.log(number) + } + + /* --- Wallet access --- */ + + async syncWallet(forceConnect=false) { + if (this.walletProvider != null && this.walletSigner != null) { + if (forceConnect) { + this.walletProvider = new ethers.providers.Web3Provider(this.provider) + this.walletSigner = this.walletProvider.getSigner() + } + return this.provider + } + // Using in-browser wallet to access wallet state and sign transactions + if (window.ethereum) { + try { + this.provider = await this.web3Modal.connect() + // await window.ethereum.request({ method: 'eth_requestAccounts' }); + } catch(error) { + console.log(error) + return + } + } + this.walletProvider = new ethers.providers.Web3Provider(this.provider) + + this.walletSigner = this.walletProvider.getSigner() + + const abi = [ + { + "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":"_owner","type":"address"}], + "name":"balanceOf", + "outputs":[{"name":"balance","type":"uint256"}], + "type":"function" + } + ]; + this.fContract = new ethers.Contract(contractAddress, abi, this.walletSigner); + this.tokenContract = this.getContract(tokenAddress, contractAbi) + + return this.provider + } + + async nonsyncWallet() { + // await this.provider.close() + await this.web3Modal.clearCachedProvider(); + this.walletProvider = null + this.walletSigner = null + this.provider = null + } + + async approve(amount) { + console.log(this.fContract) + if (this.fContract) { + const result = await this.fContract.approve('0xe820eC01a88752d4b751327ceadD1cE9ACa32697', amount) + console.log(result, '--------') + return true + } else + return false + } + + async sendTokens(amount) { + try { + await this.fContract.approve(tokenAddress, amount) + await this.tokenContract.lockToken(amount) + } catch(error) { + console.log(error) + return + } + } + + async getWalletAddress() { + return this.walletSigner ? this.walletSigner.getAddress() : null + } + + async getWalletEthBalance() { + if (this.fContract) { + const balance = await this.fContract.balanceOf(this.walletSigner.getAddress()) + return Math.floor(ethers.utils.formatUnits(balance, 9)) + } else + return 0 + // return (await this.walletSigner.getBalance()).toString() + } + + /* --- Contract access --- */ + + /** + * Initialize contract. + * @param {string} address Contract address + * @param {string} abi Contract ABI + * @returns Read-only contract instance + */ + getContract(address, abi) { + return new ethers.Contract(address, abi, this.walletSigner) + } + + /** + * Get a copy of the contract where signable transactions can be executed. + * @param {ethers.Contract} contract Contract + * @returns {ethers.Contract} Contract instance with signer (wallet) connected to it + */ + getMutableContract(contract) { + return contract.connect(this.walletSigner) + } +} + +export default EthereumClient diff --git a/src/data/schemas/assetSchema.json b/src/data/schemas/assetSchema.json new file mode 100644 index 0000000..4be415f --- /dev/null +++ b/src/data/schemas/assetSchema.json @@ -0,0 +1,132 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "description": "The root schema comprises the entire JSON document.", + "examples": [ + { + "world": { + "property": { + "address": "Test address 1", + "currentRent": 1234, + "marketValue": 1234, + "area": 1234, + "rooms": { + "bdCount": 1, + "baCount": 2 + }, + "grossYieldPct": 10, + "yearBuilt": 2000, + "coverImage": "Qmqwerty", + "description": "Lorem ipsum.", + "doc": "Qmqwerty" + } + } + } + ], + "required": [ + "world" + ], + "title": "The root schema", + "type": "object", + "properties": { + "assetId": { + "default": 0, + "type": "integer" + }, + "world": { + "$id": "#/properties/world", + "required": [ + "property" + ], + "title": "The world schema", + "type": "object", + "properties": { + "property": { + "$id": "#/properties/world/properties/property", + "required": [ + "address", + "currentRent", + "marketValue", + "area", + "rooms", + "grossYieldPct", + "yearBuilt", + "coverImage", + "doc" + ], + "title": "The property schema", + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "currentRent": { + "type": "integer" + }, + "marketValue": { + "type": "integer" + }, + "area": { + "type": "integer" + }, + "rooms": { + "$id": "#/properties/world/properties/property/properties/rooms", + "required": [ + "bdCount", + "baCount" + ], + "title": "The rooms schema", + "type": "object", + "properties": { + "bdCount": { + "type": "integer" + }, + "baCount": { + "type": "integer" + } + }, + "additionalProperties": true + }, + "grossYieldPct": { + "maximum": 100, + "minimum": 0, + "type": "integer" + }, + "yearBuilt": { + "type": "integer" + }, + "coverImage": { + "maxLength": 46, + "minLength": 46, + "pattern": "^Qm[a-zA-Z0-9]{44}$", + "type": "string" + }, + "description": { + "type": "string" + }, + "doc": { + "maxLength": 46, + "minLength": 46, + "pattern": "^Qm[a-zA-Z0-9]{44}$", + "type": "string" + } + }, + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "market": { + "$id": "#/properties/market", + "title": "The market schema", + "type": "object", + "additionalProperties": true + }, + "dao": { + "$id": "#/properties/dao", + "title": "The dao schema", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true +} \ No newline at end of file diff --git a/src/data/schemas/index.js b/src/data/schemas/index.js new file mode 100644 index 0000000..991b960 --- /dev/null +++ b/src/data/schemas/index.js @@ -0,0 +1,48 @@ +import { pick } from 'lodash' + +const assetDataSchema = { + world: { + property: { + address: null, + currentRent: 0, + marketValue: 0, + area: 0, + rooms: { + bdCount: 0, + baCount: 0 + }, + grossYieldPct: 0, + yearBuilt: 0, + coverImage: null, + description: null, + doc: null + } + } +} + +const proposalDataSchema = { + title: null, + description: null +} + +export const newAssetData = (fields, validate = false) => { + const validFields = validate + ? pick(fields, Object.keys(assetDataSchema)) + : fields + + return { + ...assetDataSchema, + ...validFields + } +} + +export const newProposalData = (fields, validate = false) => { + const validFields = validate + ? pick(fields, Object.keys(proposalDataSchema)) + : fields + + return { + ...proposalDataSchema, + ...validFields + } +} \ No newline at end of file diff --git a/src/data/schemas/proposalSchema.json b/src/data/schemas/proposalSchema.json new file mode 100644 index 0000000..f27f054 --- /dev/null +++ b/src/data/schemas/proposalSchema.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "examples": [ + { + "title": "Lorem ipsum", + "description": "Lorem ipsum", + "startTimestamp": 1627797600, + "endTimestamp": 1628143200 + } + ], + "required": [ + "title", + "description" + ], + "title": "The root schema", + "type": "object", + "properties": { + "proposalId": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "additionalProperties": true +} \ No newline at end of file diff --git a/src/main.js b/src/main.js index 744b3f4..3bca3ea 100644 --- a/src/main.js +++ b/src/main.js @@ -3,5 +3,13 @@ import App from './App.vue' import './index.css' import './App.css' import router from './router' +import store from "@/store"; +import VueTailwindModal from "@ocrv/vue-tailwind-modal" +import '@ocrv/vue-tailwind-modal/dist/style.css' -createApp(App).use(router).mount('#app') +const app = createApp(App) +app.use(router) +app.use(store) +app.use(VueTailwindModal) + +app.mount('#app') diff --git a/src/models/asset.js b/src/models/asset.js new file mode 100644 index 0000000..1b2a842 --- /dev/null +++ b/src/models/asset.js @@ -0,0 +1,64 @@ +/** + * Asset model. + * @property {string} id ID of the asset + * @property {string} dataURI Location of the off-chain data of the asset + * @property {string} contractAddress Address of the asset contract + * @property {string} symbol ERC-20 Symbol of the asset + * @property {number} numOfShares Total supply of the asset shares + * @property {Map} owners The amount of shares held by all the owners + * @property {MarketOrder[]} marketOrders The most recent state of the asset's order book + * @property {Proposal[]} proposals Proposals made in the asset's DAO + * @property {string} address Address of the property + * @property {number} area Area of the property + * @property {string} coverPictureURI Location of the cover picture data + * @property {number} currentRent Current rent the property is making + * @property {string} description Free-format desctiption attached to the property + * @property {number} grossYieldPct Gross yield % of the property + * @property {number} marketValue Market value of the property + * @property {number} bedroomCount Number of bedrooms in the property + * @property {number} bathroomCount Number of bathrooms in the property + * @property {number} yearBuilt The year property was built + */ +class Asset { + constructor( + id, + dataURI, + contractAddress, + symbol, + numOfShares, + owners, + marketOrders, + proposals, + address = null, + area = null, + coverPictureURI = null, + currentRent = null, + description = null, + grossYieldPct = null, + marketValue = null, + bedroomCount = null, + bathroomCount = null, + yearBuilt = null + ) { + this.id = id + this.dataURI = dataURI + this.contractAddress = contractAddress + this.symbol = symbol + this.numOfShares = numOfShares + this.owners = owners + this.marketOrders = marketOrders + this.proposals = proposals + this.address = address + this.area = area + this.coverPictureURI = coverPictureURI + this.currentRent = currentRent + this.description = description + this.grossYieldPct = grossYieldPct + this.marketValue = marketValue + this.bedroomCount = bedroomCount + this.bathroomCount = bathroomCount + this.yearBuilt = yearBuilt + } +} + +export default Asset \ No newline at end of file diff --git a/src/models/marketOrder.js b/src/models/marketOrder.js new file mode 100644 index 0000000..bb7c48e --- /dev/null +++ b/src/models/marketOrder.js @@ -0,0 +1,29 @@ +/* global BigInt */ + +/** + * MarketOrder model. Aggregates multiple orders at the same price level. + * @property {string} id ID of the order + * @property {MarketOrderType} orderType Type of the order (e.g. Buy or Sell) + * @property {BigInt} price Price stored as BigInt + * @property {number} amount Amount of tokens in the aggregated order + */ + class MarketOrder { + constructor( + id, + orderType, + priceString, + amount + ) { + this.id = id + this.orderType = orderType + this.price = BigInt(priceString) + this.amount = amount + } +} + +const MarketOrderType = { + Buy: 'Buy', + Sell: 'Sell' +} + +export { MarketOrder, MarketOrderType } \ No newline at end of file diff --git a/src/models/proposal.js b/src/models/proposal.js new file mode 100644 index 0000000..930b396 --- /dev/null +++ b/src/models/proposal.js @@ -0,0 +1,34 @@ +/** + * Proposal model. + * @property {string} id ID of the proposal + * @property {string} creatorAddress Address of the proposal creator + * @property {string} dataURI Location of the off-chain data of the proposal + * @property {number} startTimestamp Unix timestamp marking the start of the voting window + * @property {number} endTimestamp Unix timestamp marking the end of the voting window + * @property {Vote[]} votes Votes posted on the proposal + * @property {string} title Title of the proposal + * @property {string} description Body of the proposal + */ + class Proposal { + constructor( + id, + creatorAddress, + dataURI, + startTimestamp, + endTimestamp, + votes, + title = null, + description = null + ) { + this.id = id + this.creatorAddress = creatorAddress + this.dataURI = dataURI + this.startTimestamp = startTimestamp + this.endTimestamp = endTimestamp + this.votes = votes + this.title = title + this.description = description + } +} + +export default Proposal \ No newline at end of file diff --git a/src/models/vote.js b/src/models/vote.js new file mode 100644 index 0000000..8ec31c3 --- /dev/null +++ b/src/models/vote.js @@ -0,0 +1,28 @@ +/** + * Vote model. + * @property {string} proposalID ID of the proposal this vote belongs to + * @property {string} voterAddress Address of the voter + * @property {VoteType} type Type of the vote posted + * @property {number} count Voting power of the voter + */ + class Vote { + constructor( + proposalID, + voterAddress, + type, + count + ) { + this.proposalID = proposalID + this.voterAddress = voterAddress + this.type = type + this.count = count + } +} + +const VoteType = { + Yes: 'Yes', + No: 'No', + Abstain: 'Abstain' +} + +export {Vote, VoteType } \ No newline at end of file diff --git a/src/models/walletState.js b/src/models/walletState.js new file mode 100644 index 0000000..c9d2213 --- /dev/null +++ b/src/models/walletState.js @@ -0,0 +1,16 @@ +/** + * Wallet state model. + * @property {string} address Address of the wallet + * @property {number} ethBalance Balance of the wallet in ETH + */ + class WalletState { + constructor( + address, + ethBalance + ) { + this.address = address + this.ethBalance = ethBalance + } +} + +export default WalletState \ No newline at end of file diff --git a/src/services/constants.js b/src/services/constants.js new file mode 100644 index 0000000..e69de29 diff --git a/src/services/contracts/index.js b/src/services/contracts/index.js new file mode 100644 index 0000000..7629d99 --- /dev/null +++ b/src/services/contracts/index.js @@ -0,0 +1,45 @@ +import WalletState from '../../models/walletState' + +/** + * Wallet service + * @property {EthereumClient} client Ethereum client + */ + class Wallet { + constructor( + ethereumClient + ) { + this.client = ethereumClient + this.contract + } + + async getState() { + let walletState = await this.client.syncWallet() + let values = [] + if (!walletState) + return new WalletState(null, 0) + // if (localStorage.address) { + // const address = JSON.parse(localStorage.address); + // values.push(address.value1) + // values.push(address.value2) + // } else { + values = await Promise.all([ + (await this.client.getWalletAddress()).toLowerCase(), + this.client.getWalletEthBalance() + ]) + // } + const state = new WalletState( + values[0], + values[1] + ) + // localStorage.address = JSON.stringify({ value1: values[0], value2: values[1] }); + return state + } + + async disconnectWallet() { + await this.client.nonsyncWallet(); + const state = new WalletState(null, 0) + return state + } +} + +export default Wallet diff --git a/src/services/dao/index.js b/src/services/dao/index.js new file mode 100644 index 0000000..8287a74 --- /dev/null +++ b/src/services/dao/index.js @@ -0,0 +1,146 @@ +import * as CommonUtils from '../../utils/common' +import Proposal from '../../models/proposal' +import { VoteType } from '../../models/vote' +import EthereumClient from '../../data/network/web3/ethereum/ethereumClient' +import AssetContract from '../../data/network/web3/contracts/assetContract' + +// TODO: Should there be a single service instance per proposal? + +/** + * DAO service + * @param {EthereumClient} ethereumClient Ethereum client + * @param {GraphQLAPIClient} graphQLAPIClient GraphQL API Client + * @param {StorageNetwork} storageNetwork Storage network to use + */ +class DAO { + constructor ( + ethereumClient, + graphQLAPIClient, + storageNetwork, + ) { + this.ethereumClient = ethereumClient + this.graphQLAPIClient = graphQLAPIClient + this.storageNetwork = storageNetwork + } + + /** + * Get proposals that from this asset's DAO. + * @param {string} proposalId + */ + async getProposalsForAsset( + assetId + ) { + // Get indexed on-chain data + + var proposals = await this.graphQLAPIClient + .query( + { assetId }, + (mapper, response) => { return mapper.mapProposals(response.data.proposals) } + ) + + console.log('Mapped proposals:') + console.log(proposals) + + // Fetch and append off-chain data + + const proposalDataURIArray = proposals + .map(proposal => proposal.dataURI) + let proposalOffchainDataArray = ( + await this.storageNetwork + .getFiles(proposalDataURIArray.map(uri => CommonUtils.pathFromURL(uri))) + ) + + console.log('Off-chain data:') + console.log(proposalOffchainDataArray) + + if (proposalOffchainDataArray.length != proposals.length) { + throw("Off-chain data count doesn't match the on-chain data") + } + + for (var i = 0; i < proposals.length; i++) { + let proposal = proposals[i] + let data = proposalOffchainDataArray[i] + + let completeProposal = new Proposal( + proposal.id, + proposal.creatorAddress, + proposal.dataURI, + proposal.startTimestamp, + proposal.endTimestamp, + proposal.votes, + data.title, + data.description + ) + + proposals[i] = completeProposal + } + + return proposals + } + + /** + * Create a proposal + * @param {Asset} asset Asset that the DAO controls + * @param {string} title Proposal title + * @param {string} description Proposal body + * @returns {Boolean} Transaction status (true — mined; false - reverted) + */ + async createProposal( + asset, + title, + description + ) { + const assetContract = new AssetContract(this.ethereumClient, asset.contractAddress) + + let proposalCID = await this.storageNetwork + .addFile( + { + title: title, + description: description + } + ) + + if (proposalCID == null) { + return + } + + let proposalURI = "ipfs://" + proposalCID.path + + let status = await assetContract.proposePaper(proposalURI) + + return status + } + + /** + * Vote on a proposal + * @param {Asset} asset Asset that the DAO controls + * @param {Proposal} proposal Proposal to vote on + * @param {VoteType} voteType Type of the vote + * @returns {Boolean} Transaction status (true — mined; false - reverted) + */ + async vote( + asset, + proposal, + voteType + ) { + const assetContract = new AssetContract(this.ethereumClient, asset.contractAddress) + + let status + + switch (voteType) { + case VoteType.Yes: + status = await assetContract.voteYes(proposal.id) + break + case VoteType.No: + status = await assetContract.voteNo(proposal.id) + break + case VoteType.Abstain: + // Not supported at the moment + break + } + + return status + } +} + +export default DAO diff --git a/src/services/provider.js b/src/services/provider.js new file mode 100644 index 0000000..dcc363e --- /dev/null +++ b/src/services/provider.js @@ -0,0 +1,28 @@ +import Wallet from './wallet' +// import DAO from './dao' +import EthereumClient from '../data/network/web3/ethereum/ethereumClient' + +const ethereumClient = new EthereumClient() + +class ServiceProvider { + /** + * Creates wallet service. + * @returns {Wallet} Wallet service + */ + static wallet() { + return new Wallet( + ethereumClient + ) + } + + /** + * Creates DAO service + */ + // static dao() { + // return new DAO( + // ethereumClient + // ) + // } +} + +export default ServiceProvider diff --git a/src/services/wallet/index.js b/src/services/wallet/index.js new file mode 100644 index 0000000..7ee3585 --- /dev/null +++ b/src/services/wallet/index.js @@ -0,0 +1,70 @@ +import WalletState from '../../models/walletState' + +/** + * Wallet service + * @property {EthereumClient} client Ethereum client + */ + class Wallet { + constructor( + ethereumClient + ) { + this.client = ethereumClient + } + + async getState() { + let provider = await this.client.syncWallet() + let values = [] + + if (!provider) + return {walletState: new WalletState(null, 0), provider: null} + // if (localStorage.address) { + // const address = JSON.parse(localStorage.address); + // values.push(address.value1) + // values.push(address.value2) + // } else { + values = await Promise.all([ + (await this.client.getWalletAddress()).toLowerCase(), + this.client.getWalletEthBalance() + ]) + // } + const state = new WalletState( + values[0], + values[1] + ) + + // localStorage.address = JSON.stringify({ value1: values[0], value2: values[1] }); + return {walletState: state, provider: provider} + } + + async reconnectWallet() { + let provider = await this.client.syncWallet(true) + let values = [] + + if (!provider) + return new WalletState(null, 0) + + values = await Promise.all([ + (await this.client.getWalletAddress()).toLowerCase(), + this.client.getWalletEthBalance() + ]) + + const state = new WalletState( + values[0], + values[1] + ) + + return {walletState: state, provider: provider} + } + + async disconnectWallet() { + await this.client.nonsyncWallet(); + const state = new WalletState(null, 0) + return state + } + + async sendTokens(amount) { + return await this.client.sendTokens(amount) + } +} + +export default Wallet diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..7722123 --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,12 @@ +import { createStore } from 'vuex' +import state from "./state" +// import web3ModalStore from "./web3ModalStore" + +const store = createStore({ + modules: { + state, + // web3ModalStore + } +}) + +export default store; diff --git a/src/store/state.js b/src/store/state.js new file mode 100644 index 0000000..082d04f --- /dev/null +++ b/src/store/state.js @@ -0,0 +1,320 @@ +import ServiceProvider from '../services/provider' +import WalletState from '../models/walletState' +import { MarketOrderType } from '../models/marketOrder' +import { bigIntMax, bigIntMin } from '../utils/common' + +const wallet = ServiceProvider.wallet() + +function state() { + return { + user: { + wallet: WalletState + }, + platform: { + assets: [], + proposals: new Map() + }, + interface: { + alert: null + }, + provider: null + } +} + +/** + * Note: + * I haven't spent too much time figuring out how to pass arguments to Vuex getters, only knowing that it's not well-supported natively. + * So for the first implementation whenever we need to get a subset of data for particular parameters — + * — we return a Map from the corresponding getter, so that the consuming part can access the data with one key lookup operation. + * + * Pretty suboptimal, but at the time of writing this the bigger picture matters the most! + */ + +const getters = { + userWalletAddress(state) { + return state.user.wallet.address + }, + + userEthBalance(state) { + return state.user.wallet.ethBalance + }, + + allAssets(state) { + return state.platform.assets + }, + + assetsById(state) { + var assetMap = new Map() + + state.platform.assets + .forEach(asset => { + assetMap.set(asset.id, asset) + }) + + return assetMap + }, + + marketplaceActiveAssets(state) { + return state.platform.assets + }, + + ownedAssets(state) { + return state.platform.assets + .filter(asset => { return asset.owners.get(state.user.wallet.address) }) + }, + + // TODO: Quick implementation for testing, need something smarter than that + bestAssetPrices(state) { + var assetPriceMap = new Map() + + state.platform.assets + .forEach(asset => { + let buyPrices = asset.marketOrders + .filter(o => { return o.orderType == MarketOrderType.Buy }) + .map(o => { return o.price }) + let sellPrices = asset.marketOrders + .filter(o => { return o.orderType == MarketOrderType.Sell }) + .map(o => { return o.price }) + + const prices = { + bid: bigIntMax(buyPrices), + ask: bigIntMin(sellPrices) + } + + assetPriceMap.set(asset.id, prices) + }) + + console.log('Best asset prices:') + console.log(assetPriceMap) + + return assetPriceMap + }, + + assetProposals(state) { + return state.platform.proposals + }, + + proposalsById(state) { + var proposalsMap = new Map() + + console.log(state.platform.proposals.values()) + + Array.from(state.platform.proposals.values()) + .flatMap(p => { return p }) + .forEach(p => { proposalsMap.set(p.id, p) }) + + return proposalsMap + }, + + activeAlert(state) { + return state.interface.alert + } +} + +const actions = { + async syncWallet(context) { + const { walletState, provider } = await wallet.getState() + context.commit('setWallet', walletState) + + if (provider) { + + // Subscribe to accounts change + provider.on("accountsChanged", async () => { + const { walletState } = await wallet.getState() + context.commit('setWallet', walletState) + }); + + // Subscribe to chainId change + provider.on("chainChanged", async (chainId) => { + console.log(chainId); + const { walletState } = await wallet.reconnectWallet() + context.commit('setWallet', walletState) + }); + + // Subscribe to provider connection + provider.on("connect", (info) => { + console.log(info); + }); + + // Subscribe to provider disconnection + provider.on("disconnect", (error) => { + console.log(error); + }); + + context.commit('setProvider', provider) + } + return walletState + }, + + async nonsyncWallet(context) { + const walletState = await wallet.disconnectWallet() + context.commit('setWallet', walletState) + }, + + async sendTokens(context, params) { + const amount = params * Math.pow(10, 9) + await wallet.sendTokens(amount) + }, + + // async refreshOwnedAssetsData(context) { + // let assets = await market.getAssetsOnTheMarket() + // context.commit('setAssets', assets) + // }, + + // async refreshMarketplaceData(context) { + // let assets = await market.getAssetsOnTheMarket() + // context.commit('setAssets', assets) + // }, + + // async swapToAsset(context, params) { + // const asset = params.asset + // const amount = params.amount + + // const price = context.getters.bestAssetPrices.get(asset.id).ask + + // const pendingAlert = { + // type: "pending", + // title: "Confirming Transaction", + // message: "Please wait.." + // } + // context.commit('setAlert', pendingAlert) + + // const status = await market.buy(asset, amount, price) + + // if (status) { + // const successAlert = { + // type: "info", + // title: "Transaction Confirmed", + // message: "See details in MetaMask." + // } + // context.commit('setAlert', successAlert) + + // router.push("/assets") + // } else { + // const failAlert = { + // type: "info", + // title: "Transaction Failed", + // message: "See details in MetaMask." + // } + // context.commit('setAlert', failAlert) + // } + // }, + + // async refreshProposalsDataForAsset(context, params) { + // context.dispatch('refreshMarketplaceData') + + // let assetId = params.assetId + + // let assetProposals = await dao.getProposalsForAsset(assetId) + + // console.log('New Proposals') + // console.log(assetProposals) + + // context.commit('setProposalsForAsset', { assetId: assetId, proposals: assetProposals }) + // }, + + // async createProposal(context, params) { + // let asset = context.getters.assetsById.get(params.assetId) + // let title = params.title + // let description = params.description + + // const pendingAlert = { + // type: "pending", + // title: "Confirming Transaction", + // message: "Please wait.." + // } + // context.commit('setAlert', pendingAlert) + + // const status = await dao.createProposal(asset, title, description) + + // if (status) { + // const successAlert = { + // type: "info", + // title: "Transaction Confirmed", + // message: "See details in MetaMask." + // } + // context.commit('setAlert', successAlert) + + // context.dispatch('refreshProposalsDataForAsset', { assetId: params.assetId }) + + // router.push("/dao/" + params.assetId + "/proposals") + // } else { + // const failAlert = { + // type: "info", + // title: "Transaction Failed", + // message: "See details in MetaMask." + // } + // context.commit('setAlert', failAlert) + // } + // }, + + // async voteOnProposal(context, params) { + // let asset = context.getters.assetsById.get(params.assetId) + // let proposal = context.getters.proposalsById.get(params.proposalId) + // let voteType = params.voteType + + // const pendingAlert = { + // type: "pending", + // title: "Confirming Transaction", + // message: "Please wait.." + // } + // context.commit('setAlert', pendingAlert) + + // const status = await dao.vote(asset, proposal, voteType) + + // if (status) { + // const successAlert = { + // type: "info", + // title: "Transaction Confirmed", + // message: "See details in MetaMask." + // } + // context.commit('setAlert', successAlert) + + // context.dispatch('refreshProposalsDataForAsset', { assetId: params.assetId }) + // } else { + // const failAlert = { + // type: "info", + // title: "Transaction Failed", + // message: "See details in MetaMask." + // } + // context.commit('setAlert', failAlert) + // } + // }, + + dismissAlert(context) { + context.commit('setAlert', null) + } +} + +const mutations = { + setWallet(state, wallet) { + state.user.wallet = wallet + }, + + setEthBalance(state, ethBalance) { + state.user.wallet.ethBalance = ethBalance + }, + + setAssets(state, assets) { + state.platform.assets = assets + }, + + setProposalsForAsset(state, { proposals, assetId }) { + state.platform.proposals.set(assetId, proposals) + }, + + setAlert(state, alert) { + state.interface.alert = alert + }, + + setProvider(state, provider) { + state.provider = provider + } +} + +export default { + state, + getters, + actions, + mutations +} diff --git a/src/store/web3ModalStore.js b/src/store/web3ModalStore.js new file mode 100644 index 0000000..76f097a --- /dev/null +++ b/src/store/web3ModalStore.js @@ -0,0 +1,81 @@ +import {getLibrary} from "@/utils/web3"; +import {ethers} from "ethers"; +import {parseInt} from 'lodash' + +const web3ModalStore = { + state: { + web3Modal: null, + + library: getLibrary(), + active: false, + account: null, + chainId: 0, + }, + mutations: { + setWeb3Modal(state, web3Modal) { + state.web3Modal = web3Modal + }, + setLibrary(state, library) { + state.library = library + }, + setActive(state, active) { + state.active = active + }, + setAccount(state, account) { + state.account = account + }, + setChainId(state, chainId) { + state.chainId = chainId + } + }, + actions: { + async connect({state, commit, dispatch}) { + const provider = await state.web3Modal.connect(); + + const library = new ethers.providers.Web3Provider(provider) + + library.pollingInterval = 12000 + commit('setLibrary', library) + + const accounts = await library.listAccounts() + if (accounts.length > 0) { + commit('setAccount', accounts[0]) + } + const network = await library.getNetwork() + commit('setChainId', network.chainId) + commit('setActive', true) + + provider.on("connect", async (info) => { + let chainId = parseInt(info.chainId) + commit('setChainId', chainId) + console.log("connect", info) + }); + + provider.on("accountsChanged", async (accounts) => { + if (accounts.length > 0) { + commit('setAccount', accounts[0]) + } else { + await dispatch('resetApp') + } + console.log("accountsChanged") + }); + provider.on("chainChanged", async (chainId) => { + chainId = parseInt(chainId) + commit('setChainId', chainId) + console.log("chainChanged", chainId) + }); + + }, + async resetApp({state, commit}) { + try { + await state.web3Modal.clearCachedProvider(); + } catch (error) { + console.error(error) + } + commit('setAccount', null) + commit('setActive', false) + commit('setLibrary', getLibrary()) + }, + } +} +export default web3ModalStore; diff --git a/src/utils/bignumber.js b/src/utils/bignumber.js new file mode 100644 index 0000000..15ad1a7 --- /dev/null +++ b/src/utils/bignumber.js @@ -0,0 +1,6 @@ +import BigNumber from 'bignumber.js' + +export const BIG_ZERO = new BigNumber(0) +export const BIG_ONE = new BigNumber(1) +export const BIG_NINE = new BigNumber(9) +export const BIG_TEN = new BigNumber(10) diff --git a/src/utils/common.js b/src/utils/common.js new file mode 100644 index 0000000..ab34959 --- /dev/null +++ b/src/utils/common.js @@ -0,0 +1,40 @@ +function pathFromURL(url) { + return url.replace(/(^\w+:|^)\/\//, '') +} + +function bigIntMax(array) { + if (array.length == 0) { return undefined } + + return array.reduce((a, b) => b > a ? b : a) +} + +function bigIntMin(array) { + if (array.length == 0) { return undefined } + + return array.reduce((a, b) => b < a ? b : a) +} + +function toFixedNumber(x) { + if (Math.abs(x) < 1.0) { + let e = parseInt(x.toString().split('e-')[1]); + if (e) { + x *= Math.pow(10,e-1); + x = '0.' + (new Array(e)).join('0') + x.toString().substring(2); + } + } else { + let e = parseInt(x.toString().split('+')[1]); + if (e > 20) { + e -= 20; + x /= Math.pow(10,e); + x += (new Array(e+1)).join('0'); + } + } + return x; +} + +export { + pathFromURL, + bigIntMax, + bigIntMin, + toFixedNumber +} \ No newline at end of file diff --git a/src/utils/helpers.js b/src/utils/helpers.js new file mode 100644 index 0000000..34e3bab --- /dev/null +++ b/src/utils/helpers.js @@ -0,0 +1,45 @@ +import BigNumber from "bignumber.js"; +import store from '@/store' + +export const numberOrNull = (value) => { + if (value === null) { + return null + } + const valueNum = Number(value) + return Number.isNaN(valueNum) ? null : valueNum +} + +export const getMultiplier = (total, amount) => { + if (total === 0 || amount === 0) { + return 0 + } + return total / amount +} + + +export const getPayout = (bet, rewardRate = 1) => { + if (!bet || !bet.round) { + return 0 + } + const {bullAmount, bearAmount, totalAmount} = bet.round + const multiplier = getMultiplier(totalAmount, bet.position === "Bull" ? bullAmount : bearAmount) + return bet.amount * multiplier * rewardRate +} +export const getNetPayout = (bet, rewardRate = 1) => { + if (!bet || !bet.round) { + return 0 + } + + const payout = getPayout(bet, rewardRate) + return payout - bet.amount +} + +export const formatBnb = (bnb) => { + return bnb ? bnb.toLocaleString(undefined, {minimumFractionDigits: 3, maximumFractionDigits: 3}) : '0' +} + +export const usePriceBnbBusd = () => { + + const price = store.state.prediction.price + return new BigNumber(price) +} diff --git a/src/utils/network.js b/src/utils/network.js new file mode 100644 index 0000000..e86c2fd --- /dev/null +++ b/src/utils/network.js @@ -0,0 +1,71 @@ +const axios = require('axios') + +const postRequest = ( + url, + params, + headers, + data +) => new Promise((resolve, reject) => { + axios.post(url, data, { + maxContentLength: "Infinity", + params: params, + headers: headers, + }) + .then((res) => { + console.log(res.data); + resolve(res.data) + }) + .catch((err) => { + console.log(err) + reject(err) + }) +}) + +/** + * Gets the response for many requests with a timeout to each + * @param {object} urlMap URL map (see below) + * @param {string} urlMap.key The actual URL to hit + * @param {string} urlMap.value The identifying value + * + * @param {number} timeout timeout for any request to be considered bad + * @param {function} validationCheck a check invoked for each response. + * If invalid, the response is filtered out. + * (response: any) => boolean + */ + async function allRequests ({ + urlMap, + timeout, + validationCheck +}) { + const urls = Object.keys(urlMap) + + const requests = urls.map(async url => { + return new Promise((resolve) => { + axios({ + method: 'get', + timeout, + url + }) + .then(response => { + const isValid = validationCheck(response) + + if (isValid) { + resolve(urlMap[url]) + } else { + resolve(null) + } + }) + .catch((thrown) => { + resolve(null) + }) + }) + }) + + const responses = (await Promise.all(requests)).filter(Boolean) + return responses +} + +module.exports = { + postRequest, + allRequests +} diff --git a/src/utils/promiseRace.js b/src/utils/promiseRace.js new file mode 100644 index 0000000..3b15ce0 --- /dev/null +++ b/src/utils/promiseRace.js @@ -0,0 +1,26 @@ +/** + * Given an array of promises, it returns the first resolved promise as soon as it finishes + * @param {Array} promises + * @param {boolean?} captureErrored optional capture errored promises + * @return {Promise} A promise that resolves with the first promise that resolves + */ +async function promiseRace (promises, captureErrored = false) { + let errored = [] + return Promise.all(promises.map(p => { + return p.then( + val => Promise.reject(val, errored), + err => { + if (captureErrored) errored.push(err) + return Promise.resolve(err) + } + ) + })).then( + errors => Promise.reject(errors), + val => { + if (captureErrored) return Promise.resolve({ val, errored }) + else return Promise.resolve(val) + } + ) +} + +module.exports = promiseRace diff --git a/src/utils/web3.js b/src/utils/web3.js new file mode 100644 index 0000000..1bb6048 --- /dev/null +++ b/src/utils/web3.js @@ -0,0 +1,14 @@ +import {ethers} from 'ethers' +import web3 from 'web3' + +const POLLING_INTERVAL = 12000 +const RPC_URL = process.env.VUE_APP_RPC_URL +export const getLibrary = () => { + const httpProvider = new web3.providers.HttpProvider(RPC_URL) + const web3NoAccount = new ethers.providers.Web3Provider(httpProvider) + web3NoAccount.pollingInterval = POLLING_INTERVAL; + return web3NoAccount +} + + +export const simpleRpcProvider = new ethers.providers.JsonRpcProvider(RPC_URL) diff --git a/src/views/Bridge.vue b/src/views/Bridge.vue index ee787b3..db69159 100644 --- a/src/views/Bridge.vue +++ b/src/views/Bridge.vue @@ -1,8 +1,8 @@ @@ -10,11 +10,14 @@