add enrichment of polls

fetch token balance from subgraph
This commit is contained in:
Barry Gitarts 2020-07-30 10:57:43 -04:00
parent c330e1cb34
commit f81c6a0669
10 changed files with 312 additions and 50 deletions

View File

@ -1,15 +1,14 @@
import React, { useState, useEffect } from 'react'
import { Route, Link, Switch, HashRouter as Router } from 'react-router-dom'
import { ThemeProvider } from '@material-ui/core/styles'
import EmbarkJS from './embarkArtifacts/embarkjs'
import theme from './styles/theme'
import useStyles from './styles/app'
import { SetAccount } from './types'
import Header from './components/Header'
import CreatePoll from './components/CreatePoll'
import ListPolls from './components/ListPolls'
import Poll from './components/Poll'
import { MessagesProvider } from './context/messages/context'
import { grabAddress, enableEthereum } from './utils/network'
declare global {
interface Window {
@ -18,41 +17,6 @@ declare global {
}
}
function grabAddress(setAccount: SetAccount): void {
if (window.ethereum) {
accountListener(setAccount)
const { selectedAddress: account } = window.ethereum
if (account) setAccount(account)
} else {
console.log('window.ethereum not found :', {window})
}
}
function accountListener(setAccount: SetAccount): void {
// Not supported in status. Metamask supported
try {
window.ethereum.on('accountsChanged', function (accounts: string[]) {
const [account] = accounts
setAccount(account)
})
} catch (error) {
console.error('accountsChanged listener : ', {error})
}
}
async function enableEthereum(setAccount: SetAccount): Promise<string | undefined> {
try {
const accounts = await EmbarkJS.enableEthereum();
const account = accounts[0]
setAccount(account)
// TODO get balances across all relvant tokens
//this.getAndSetBalances(account)
return account
} catch (error) {
console.error('Enable Ethereum :', {error})
}
}
function App() {
const classes: any = useStyles()
const [account, setAccount] = useState('')

View File

@ -1,4 +1,4 @@
import React, { Fragment, useContext } from 'react'
import React, { Fragment, useContext, useEffect } from 'react'
import useStyles from '../styles/poll'
import { useParams } from "react-router-dom"
import { MessagesContext } from '../context/messages/context'
@ -6,6 +6,80 @@ import Typography from '@material-ui/core/Typography'
import TextField from '@material-ui/core/TextField'
import MenuItem from '@material-ui/core/MenuItem'
import { Formik, FormikProps } from 'formik'
import StatusButton from './base/Button'
import { sendToPublicChat } from '../utils/status'
import { prettySign } from '../utils/signing'
import Divider from '@material-ui/core/Divider'
import {
gotoPublicChat,
getChatMessages,
useChatMessages
} from '../utils/status'
import { verifyMessages } from '../utils/messages'
import { Topics, IAccountSnapshotQuery, IBalanceByAddress } from '../types'
import { ApolloClient, InMemoryCache } from '@apollo/client'
import { getAccountBalances } from '../queries'
const SNT_ROPSTEN_SUBGRAPH = 'https://api.thegraph.com/subgraphs/name/bgits/status-snt'
const client = new ApolloClient({
uri: SNT_ROPSTEN_SUBGRAPH,
cache: new InMemoryCache()
})
async function gotoPoll(channel: string) {
await gotoPublicChat(channel)
getChatMessages()
}
async function verifyEnrichMessages(topics: Topics, rawKey: string, setState: Function) {
const messages = { [rawKey]: topics[rawKey] }
const verified = await verifyMessages(messages)
if (!verified) return
const enriched = await enrichVotes(verified)
const merged = { ...topics, ...enriched }
setState(merged)
}
async function enrichVotes(topics: Topics): Promise<Topics> {
const keys = Object.keys(topics)
const accounts: string[] = []
keys.map(k => {
const messages = topics[k]
messages.map(message => {
const { sigMsg } = message
if (!sigMsg || !sigMsg.address) return
// subgraph references all addresses as lowercase
accounts.push(sigMsg.address.toLowerCase())
})
})
const query = await client.query({
query: getAccountBalances,
variables: {
accounts
}
})
const { data: { accountBalanceSnapshots } } = query
const balancesByAddress: IBalanceByAddress = {}
accountBalanceSnapshots.map((res: IAccountSnapshotQuery) => {
const { account } = res
balancesByAddress[account.id] = res
})
const enrichedTopics: Topics = {}
keys.map(k => {
const messages = topics[k]
enrichedTopics[k] = messages.map(message => {
const { sigMsg } = message
if (!sigMsg || !sigMsg.address) return message
const address = sigMsg.address.toLowerCase()
const accountSnapshot: IAccountSnapshotQuery = balancesByAddress[address]
return { ...message, accountSnapshot }
})
})
return enrichedTopics
}
type IBallot = {
option: string
@ -13,29 +87,36 @@ type IBallot = {
function Poll() {
const { id } = useParams()
const topic = `poll-${id}`
const classes: any = useStyles()
const [rawMessages] = useChatMessages()
const messagesContext = useContext(MessagesContext)
const { chatMessages } = messagesContext
console.log({chatMessages})
const { chatMessages, dispatchSetTopics } = messagesContext
useEffect(() => {
const merged = { ...chatMessages, ...rawMessages }
if(dispatchSetTopics && rawMessages) verifyEnrichMessages(merged, topic, dispatchSetTopics)
}, [rawMessages])
if (!chatMessages) return <Fragment />
const selectedPoll= chatMessages['polls'].find(p => p.messageId === id)
if (!selectedPoll || !selectedPoll.pollInfo) return <Fragment />
const { pollInfo } = selectedPoll
const { description, title, subtitle, pollOptions } = pollInfo
const options = pollOptions.split(',')
// TODO Display ^above data
// TODO Add method to vote
//TODO vote options are drop down menu
// TODO Add method to tabulate votes
console.log({selectedPoll, options})
// get all votes, filter by messageId, filter out not verified, grab all addresses, get balances, enrich votes with balances
console.log({messagesContext, rawMessages})
return (
<Formik
initialValues={{
option: ''
}}
onSubmit={(values) => {
console.log({values})
onSubmit={async (values) => {
const signed = await prettySign(values.option)
const stringified = JSON.stringify(signed)
await sendToPublicChat(topic, stringified)
console.log({values, signed, topic})
}}
>
{({
@ -65,6 +146,18 @@ function Poll() {
</MenuItem>
))}
</TextField>
<StatusButton
className={classes.button}
buttonText="Vote"
onClick={handleSubmit}
/>
<Divider className={classes.divider} />
<StatusButton
type="button"
className={classes.button}
buttonText="Goto room and get poll results"
onClick={() => gotoPoll(topic)}
/>
</form>
)}
}

View File

@ -46,16 +46,18 @@ type ButtonProps = {
buttonText: string,
confirmed?: boolean,
loading?: boolean,
onClick: any
onClick: any,
type?: "button" | "reset" | "submit"
}
function StatusButton(props: ButtonProps) {
const { className, disabled, buttonText, confirmed, loading, onClick } = props
const classes = useStyles()
const { check, formButton, disabledButton, buttonContent, progress } = classes
const buttonType = props.type ? props.type : 'submit'
return (
<Fragment>
<Button className={classnames(formButton, className)} disabled={disabled} type="submit" variant="contained" classes={{ disabled: disabledButton }} onClick={onClick}>
<Button className={classnames(formButton, className)} disabled={disabled} type={buttonType} variant="contained" classes={{ disabled: disabledButton }} onClick={onClick}>
<div className={buttonContent}>
{confirmed && <Check className={check} />}
{loading && <CircularProgress className={progress} size={24} disableShrink />}

40
dapp/src/queries.js Normal file
View File

@ -0,0 +1,40 @@
import { gql } from '@apollo/client'
export const getAccountBalances = gql`
query AccountBalances($accounts: [String!]) {
accountBalanceSnapshots(
orderBy: timestamp,
orderDirection: desc,
where: {
account_in: $accounts
}
) {
id
timestamp,
block,
amount
account{
id
}
}
}
`
export const accountBalances = gql`
query AccountBalances($addresses: [String!] = ["0x0000000000000000000000000000000000000000"]) {
accountBalanceSnapshots(first: 5,
orderBy: timestamp,
orderDirection: desc,
where: {
account_in: $addresses
}
) {
id
timestamp,
block,
amount
account{
id
}
}
}
`

View File

@ -25,6 +25,10 @@ const useStyles = makeStyles(theme => ({
dropDown: {
gridColumn: '3 / 45'
},
divider: {
gridColumn: '3 / 48',
marginTop: '2rem'
},
link: {
gridColumn: '1 / 49',
padding: 0,

View File

@ -28,7 +28,8 @@ export type Message = {
verified?: boolean,
sigMsg?: ISignedMessage
pollInfo?: IPollInfo
formattedEndDate?: IFormattedDate
formattedEndDate?: IFormattedDate,
accountSnapshot?: IAccountSnapshotQuery
}
export type Topics = {
[chat: string]: Message[]
@ -38,3 +39,19 @@ export type IFormattedDate = {
daysRemaining: number,
hoursRemaining: number
}
export type IAccountSnapshotQuery = {
_typename: "AccountBalanceSnapshot",
id: string,
amount: string,
block: string,
timestamp: string,
account: {
id: string,
_typename: "Account"
}
}
export type IBalanceByAddress = {
[address: string]: IAccountSnapshotQuery
}

View File

@ -0,0 +1,31 @@
import { Topics, Message, ISignedMessage } from '../types'
import { verifySignedMessage } from './signing'
export async function verifyMessages(
messages: Topics | undefined
): Promise<Topics | undefined> {
if (!messages) return
const fmtMessages: Topics = {}
const keys = Object.keys(messages)
keys.map(key => {
const msgs: Message[] = messages[key]
const parsed: Message[] = msgs.map(msg => {
const { text } = msg;
const newMsg = { ...msg };
try {
const sigMsg: ISignedMessage = JSON.parse(text);
newMsg['sigMsg'] = sigMsg
} catch (e) {
console.log({e})
} finally {
if (!!newMsg['sigMsg']) {
newMsg['verified'] = verifySignedMessage(newMsg['sigMsg'])
}
return newMsg
}
})
const verified = parsed.filter(m => m.verified === true)
fmtMessages[key] = verified
})
return fmtMessages
}

View File

@ -1,4 +1,7 @@
import { Network } from '../types'
import EmbarkJS from '../embarkArtifacts/embarkjs'
import { SetAccount } from '../types'
declare global {
interface web3 {
eth: any;
@ -15,3 +18,38 @@ export async function setNetwork(setState: Function) {
const network = await getNetwork()
setState(network)
}
export function grabAddress(setAccount: SetAccount): void {
if (window.ethereum) {
accountListener(setAccount)
const { selectedAddress: account } = window.ethereum
if (account) setAccount(account)
} else {
console.log('window.ethereum not found :', {window})
}
}
function accountListener(setAccount: SetAccount): void {
// Not supported in status. Metamask supported
try {
window.ethereum.on('accountsChanged', function (accounts: string[]) {
const [account] = accounts
setAccount(account)
})
} catch (error) {
console.error('accountsChanged listener : ', {error})
}
}
export async function enableEthereum(setAccount: SetAccount): Promise<string | undefined> {
try {
const accounts = await EmbarkJS.enableEthereum();
const account = accounts[0]
setAccount(account)
// TODO get balances across all relvant tokens
//this.getAndSetBalances(account)
return account
} catch (error) {
console.error('Enable Ethereum :', {error})
}
}

View File

@ -33,12 +33,14 @@
"embarkjs-whisper": "^6.0.0"
},
"dependencies": {
"@apollo/client": "^3.1.1",
"@material-ui/core": "^4.11.0",
"@material-ui/styles": "^4.10.0",
"@types/bl": "^2.1.0",
"bl": "^4.0.2",
"eth-crypto": "^1.6.0",
"ethereumjs-util": "^6.0.0",
"graphql": "^15.3.0",
"ipfs-http-client": "^44.3.0",
"react-router-dom": "^5.2.0"
}

View File

@ -2,6 +2,24 @@
# yarn lockfile v1
"@apollo/client@^3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.1.1.tgz#7d57d037be8ee93694fbf82579f703e635c836c1"
integrity sha512-c5DxrU81p0B5BsyBXm+5uPJqLCX2epnBsd87PXfRwzDLbp/NiqnWp6a6c5vT5EV2LwJuCq1movmKthoy0gFb0w==
dependencies:
"@types/zen-observable" "^0.8.0"
"@wry/context" "^0.5.2"
"@wry/equality" "^0.2.0"
fast-json-stable-stringify "^2.0.0"
graphql-tag "^2.11.0"
hoist-non-react-statics "^3.3.2"
optimism "^0.12.1"
prop-types "^15.7.2"
symbol-observable "^1.2.0"
ts-invariant "^0.4.4"
tslib "^1.10.0"
zen-observable "^0.8.14"
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.8.3":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a"
@ -1475,6 +1493,11 @@
dependencies:
"@types/node" "*"
"@types/zen-observable@^0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d"
integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg==
"@web3-js/scrypt-shim@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@web3-js/scrypt-shim/-/scrypt-shim-0.1.0.tgz#0bf7529ab6788311d3e07586f7d89107c3bea2cc"
@ -1640,6 +1663,20 @@
"@webassemblyjs/wast-parser" "1.8.5"
"@xtuc/long" "4.2.2"
"@wry/context@^0.5.2":
version "0.5.2"
resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.5.2.tgz#f2a5d5ab9227343aa74c81e06533c1ef84598ec7"
integrity sha512-B/JLuRZ/vbEKHRUiGj6xiMojST1kHhu4WcreLfNN7q9DqQFrb97cWgf/kiYsPSUCAMVN0HzfFc8XjJdzgZzfjw==
dependencies:
tslib "^1.9.3"
"@wry/equality@^0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.2.0.tgz#a312d1b6a682d0909904c2bcd355b02303104fb7"
integrity sha512-Y4d+WH6hs+KZJUC8YKLYGarjGekBrhslDbf/R20oV+AakHPINSitHfDRQz3EGcEWc1luXYNUvMhawWtZVWNGvQ==
dependencies:
tslib "^1.9.3"
"@xtuc/ieee754@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
@ -6443,6 +6480,16 @@ graceful-fs@*, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, gr
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=
graphql-tag@^2.11.0:
version "2.11.0"
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.11.0.tgz#1deb53a01c46a7eb401d6cb59dec86fa1cccbffd"
integrity sha512-VmsD5pJqWJnQZMUeRwrDhfgoyqcfwEkvtpANqcoUG8/tOLkwNgU9mzub/Mc78OJMhHjx7gfAMTxzdG43VGg3bA==
graphql@^15.3.0:
version "15.3.0"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.3.0.tgz#3ad2b0caab0d110e3be4a5a9b2aa281e362b5278"
integrity sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w==
growl@1.10.5:
version "1.10.5"
resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
@ -9476,6 +9523,13 @@ open@6.4.0:
dependencies:
is-wsl "^1.1.0"
optimism@^0.12.1:
version "0.12.1"
resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.12.1.tgz#933f9467b9aef0e601655adb9638f893e486ad02"
integrity sha512-t8I7HM1dw0SECitBYAqFOVHoBAHEQBTeKjIL9y9ImHzAVkdyPK4ifTgM4VJRDtTUY4r/u5Eqxs4XcGPHaoPkeQ==
dependencies:
"@wry/context" "^0.5.2"
optimist@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
@ -11954,6 +12008,11 @@ swarmhash@^0.1.0:
keccakjs "^0.2.3"
safe-buffer "^5.1.2"
symbol-observable@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
table@^3.7.8:
version "3.8.3"
resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f"
@ -12211,7 +12270,14 @@ triple-beam@^1.2.0, triple-beam@^1.3.0:
dependencies:
glob "^7.1.2"
tslib@^1.9.0:
ts-invariant@^0.4.4:
version "0.4.4"
resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86"
integrity sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==
dependencies:
tslib "^1.9.3"
tslib@^1.10.0, tslib@^1.9.0, tslib@^1.9.3:
version "1.13.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
@ -13563,3 +13629,8 @@ yo-yoify@4.3.0:
on-load "^3.2.0"
through2 "^2.0.1"
transform-ast "^2.2.1"
zen-observable@^0.8.14:
version "0.8.15"
resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15"
integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==