+
- connect your wallet
+ {{ address ? 'Transfer To' : 'connect your wallet' }}
Swap token
@@ -15,14 +15,20 @@
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 @@