Introduce timed poll voting (#7)

This commit is contained in:
Szymon Szlachtowicz 2021-08-10 18:04:40 +02:00 committed by GitHub
parent e564350805
commit 9cfb94ece5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 278 additions and 2 deletions

View File

@ -18,8 +18,9 @@ objects of type WakuVoting expose functions:
createTimedPoll(signer: JsonRpcSigner, question:string, answers: string[], pollType: enum, minToken?: BigNumber, endTime?: number)
getTimedPolls()
sendTimedPollVote(signer: JsonRpcSigner, pollHash: string, selectedAnswer:number, sntAmount?: number)
sendTimedPollVote(signer: JsonRpcSigner, id: string, selectedAnswer:number, tokenAmount?: number)
getTimedPollVotes(id: string)
## Polls
@ -47,3 +48,22 @@ message PollInit {
bytes signature = 8 // signature of all above fields
}
```
### Voting on timed poll
To vote on poll user has to send waku message on topic:
`/{dapp name}/waku-polling/votes/proto`
Proto fields for poll vote
```proto
message TimedPollVote {
bytes id = 1; // id of a poll
bytes voter = 2; // Address of a voter
int64 timestamp = 3; // Timestamp of a waku message
int64 answer = 4; // specified poll answer
optional bytes tokenAmount = 5; // amount of token used for WEIGHTED voting
bytes signature = 6; // signature of all above fields
}
```

View File

@ -5,13 +5,15 @@ import { PollType } from './types/PollType'
import { BigNumber, Wallet } from 'ethers'
import PollInit from './utils/proto/PollInit'
import { WakuMessage } from 'js-waku'
import { TimedPollVoteMsg } from './models/TimedPollVoteMsg'
import TimedPollVote from './utils/proto/TimedPollVote'
class WakuVoting {
private appName: string
private waku: Waku | undefined
public tokenAddress: string
private pollInitTopic: string
private timedPollVoteTopic: string
private static async createWaku() {
const waku = await Waku.create()
const nodes = await getStatusFleetNodes()
@ -29,6 +31,7 @@ class WakuVoting {
this.appName = appName
this.tokenAddress = tokenAddress
this.pollInitTopic = `/${this.appName}/waku-polling/timed-polls-init/proto/`
this.timedPollVoteTopic = `/${this.appName}/waku-polling/votes/proto/`
this.waku = waku
}
@ -64,6 +67,30 @@ class WakuVoting {
.map((msg) => PollInit.decode(msg.payload, msg.timestamp))
.filter((poll): poll is PollInitMsg => !!poll)
}
public async sendTimedPollVote(
signer: JsonRpcSigner | Wallet,
id: string,
selectedAnswer: number,
tokenAmount?: BigNumber
) {
const pollVote = await TimedPollVoteMsg.create(signer, id, selectedAnswer, tokenAmount)
const payload = TimedPollVote.encode(pollVote)
if (payload && pollVote) {
const wakuMessage = await WakuMessage.fromBytes(payload, this.timedPollVoteTopic, {
timestamp: new Date(pollVote.timestamp),
})
await this.waku?.relay.send(wakuMessage)
}
}
public async getTimedPollVotes(id: string) {
const messages = await this.waku?.store.queryHistory({ contentTopics: [this.timedPollVoteTopic] })
return messages
?.filter((e): e is WakuMessage & { payload: Uint8Array } => !!e?.payload)
.map((msg) => TimedPollVote.decode(msg.payload, msg.timestamp))
.filter((poll): poll is TimedPollVoteMsg => !!poll && poll.id === id)
}
}
export default WakuVoting

View File

@ -0,0 +1,74 @@
import { BigNumber, utils } from 'ethers'
import { JsonRpcSigner } from '@ethersproject/providers'
import { TimedPollVote } from 'protons'
import { Wallet } from 'ethers'
export class TimedPollVoteMsg {
public id: string
public voter: string
public timestamp: number
public answer: number
public tokenAmount?: BigNumber
public signature: string
private constructor(
id: string,
voter: string,
timestamp: number,
answer: number,
signature: string,
tokenAmount?: BigNumber
) {
this.id = id
this.voter = voter
this.timestamp = timestamp
this.answer = answer
this.tokenAmount = tokenAmount
this.signature = signature
}
static async create(
signer: JsonRpcSigner | Wallet,
id: string,
answer: number,
tokenAmount?: BigNumber
): Promise<TimedPollVoteMsg> {
const voter = await signer.getAddress()
const timestamp = Date.now()
const msg: (string | number | BigNumber)[] = [id, voter, timestamp, answer]
const types = ['bytes32', 'address', 'uint256', 'uint64']
if (tokenAmount) {
msg.push(tokenAmount)
types.push('uint256')
}
const packedData = utils.arrayify(utils.solidityPack(types, msg))
const signature = await signer.signMessage(packedData)
return new TimedPollVoteMsg(id, voter, timestamp, answer, signature, tokenAmount)
}
static fromProto(payload: TimedPollVote) {
const id = utils.hexlify(payload.id)
const voter = utils.getAddress(utils.hexlify(payload.voter))
const timestamp = payload.timestamp
const answer = payload.answer
const signature = utils.hexlify(payload.signature)
const tokenAmount = payload.tokenAmount ? BigNumber.from(payload.tokenAmount) : undefined
const msg: (string | number | BigNumber)[] = [id, voter, timestamp, answer]
const types = ['bytes32', 'address', 'uint256', 'uint64']
if (tokenAmount) {
msg.push(tokenAmount)
types.push('uint256')
}
const packedData = utils.arrayify(utils.solidityPack(types, msg))
const verifiedAddress = utils.verifyMessage(packedData, signature)
if (verifiedAddress != voter) {
return undefined
}
return new TimedPollVoteMsg(id, voter, timestamp, answer, signature, tokenAmount)
}
}

View File

@ -0,0 +1,50 @@
import protons, { TimedPollVote } from 'protons'
import { utils } from 'ethers'
import { TimedPollVoteMsg } from '../../models/TimedPollVoteMsg'
const proto = protons(`
message TimedPollVote {
bytes id = 1; // id of a poll
bytes voter = 2; // Address of a voter
int64 timestamp = 3; // Timestamp of a waku message
int64 answer = 4; // specified poll answer
optional bytes tokenAmount = 5; // amount of token used for WEIGHTED voting
bytes signature = 6; // signature of all above fields
}
`)
export function encode(timedPollVote: TimedPollVoteMsg) {
try {
const arrayify = utils.arrayify
const voteProto: TimedPollVote = {
id: arrayify(timedPollVote.id),
voter: arrayify(timedPollVote.voter),
timestamp: timedPollVote.timestamp,
answer: timedPollVote.answer,
tokenAmount: timedPollVote.tokenAmount ? arrayify(timedPollVote.tokenAmount) : undefined,
signature: arrayify(timedPollVote.signature),
}
return proto.TimedPollVote.encode(voteProto)
} catch {
return undefined
}
}
export function decode(payload: Uint8Array, timestamp: Date | undefined) {
try {
const msg = proto.TimedPollVote.decode(payload)
if (!timestamp || timestamp.getTime() != msg.timestamp) {
return undefined
}
if (msg.id && msg.voter && msg.timestamp && msg.answer != undefined && msg.signature) {
return TimedPollVoteMsg.fromProto(msg)
}
} catch {
return undefined
}
return undefined
}
export default { encode, decode }

View File

@ -0,0 +1,49 @@
import { expect } from 'chai'
import { TimedPollVoteMsg } from '../../src/models/TimedPollVoteMsg'
import { MockProvider } from 'ethereum-waffle'
import { BigNumber, utils } from 'ethers'
describe('TimedPollVoteMsg', () => {
const provider = new MockProvider()
const [alice] = provider.getWallets()
const pollId = '0x14c336ef626274f156d094fc1d7ffad2bbc83cccc9817598dd55e42a86b56b72'
it('success', async () => {
const poll = await TimedPollVoteMsg.create(alice, pollId, 0)
expect(poll).to.not.be.undefined
expect(poll.voter).to.eq(alice.address)
expect(poll.answer).to.eq(0)
expect(poll.id).to.be.eq(pollId)
expect(poll.tokenAmount).to.be.undefined
const msg: (string | number | BigNumber)[] = [poll.id, poll.voter, poll.timestamp, poll.answer]
const types = ['bytes32', 'address', 'uint256', 'uint64']
const packedData = utils.arrayify(utils.solidityPack(types, msg))
const verifiedAddress = utils.verifyMessage(packedData, poll.signature)
expect(verifiedAddress).to.eq(alice.address)
})
it('success token amount', async () => {
const poll = await TimedPollVoteMsg.create(alice, pollId, 1, BigNumber.from(100))
expect(poll).to.not.be.undefined
expect(poll.voter).to.eq(alice.address)
expect(poll.answer).to.eq(1)
expect(poll.id).to.be.eq(pollId)
expect(poll.tokenAmount).to.deep.eq(BigNumber.from(100))
const msg: (string | number | BigNumber | undefined)[] = [
poll.id,
poll.voter,
poll.timestamp,
poll.answer,
poll?.tokenAmount,
]
const types = ['bytes32', 'address', 'uint256', 'uint64', 'uint256']
const packedData = utils.arrayify(utils.solidityPack(types, msg))
const verifiedAddress = utils.verifyMessage(packedData, poll.signature)
expect(verifiedAddress).to.eq(alice.address)
})
})

View File

@ -0,0 +1,43 @@
import { expect } from 'chai'
import TimedPollVote from '../../../src/utils/proto/TimedPollVote'
import { BigNumber } from 'ethers'
import { TimedPollVoteMsg } from '../../../src/models/TimedPollVoteMsg'
import { MockProvider } from 'ethereum-waffle'
describe('TimedPollVote', () => {
const provider = new MockProvider()
const [alice] = provider.getWallets()
const pollId = '0x14c336ef626274f156d094fc1d7ffad2bbc83cccc9817598dd55e42a86b56b72'
it('success', async () => {
const data = await TimedPollVoteMsg.create(alice, pollId, 0)
const payload = TimedPollVote.encode(data)
expect(payload).to.not.be.undefined
if (payload) {
expect(TimedPollVote.decode(payload, new Date(data.timestamp))).to.deep.eq(data)
}
})
it('random decode', async () => {
expect(TimedPollVote.decode(new Uint8Array([12, 12, 3, 32, 31, 212, 31, 32, 23]), new Date(10))).to.be.undefined
})
it('random data', async () => {
expect(TimedPollVote.encode({ sadf: '0x0' } as unknown as TimedPollVoteMsg)).to.be.undefined
})
it('data with token', async () => {
const data = await TimedPollVoteMsg.create(alice, pollId, 0, BigNumber.from(120))
const payload = TimedPollVote.encode(data)
expect(payload).to.not.be.undefined
if (payload) {
expect(TimedPollVote.decode(payload, new Date(data.timestamp))).to.deep.eq({
...data,
tokenAmount: BigNumber.from(120),
})
}
})
})

View File

@ -14,11 +14,24 @@ declare module 'protons' {
signature: Uint8Array
}
export type TimedPollVote = {
id: Uint8Array
voter: Uint8Array
timestamp: number
answer: number
tokenAmount?: Uint8Array
signature: Uint8Array
}
function protons(init: string): {
PollInit: {
encode: (pollInit: PollInit) => Uint8Array,
decode: (payload: Uint8Array) => PollInit
}
TimedPollVote:{
encode: (timedPollVote: TimedPollVote) => Uint8Array,
decode: (payload: Uint8Array) => TimedPollVote
}
}
export = protons
}