mirror of
https://github.com/status-im/wakuconnect-vote-poll-sdk.git
synced 2025-02-24 08:08:21 +00:00
Introduce timed poll voting (#7)
This commit is contained in:
parent
e564350805
commit
9cfb94ece5
@ -18,8 +18,9 @@ objects of type WakuVoting expose functions:
|
|||||||
|
|
||||||
createTimedPoll(signer: JsonRpcSigner, question:string, answers: string[], pollType: enum, minToken?: BigNumber, endTime?: number)
|
createTimedPoll(signer: JsonRpcSigner, question:string, answers: string[], pollType: enum, minToken?: BigNumber, endTime?: number)
|
||||||
getTimedPolls()
|
getTimedPolls()
|
||||||
sendTimedPollVote(signer: JsonRpcSigner, pollHash: string, selectedAnswer:number, sntAmount?: number)
|
|
||||||
|
|
||||||
|
sendTimedPollVote(signer: JsonRpcSigner, id: string, selectedAnswer:number, tokenAmount?: number)
|
||||||
|
getTimedPollVotes(id: string)
|
||||||
|
|
||||||
## Polls
|
## Polls
|
||||||
|
|
||||||
@ -47,3 +48,22 @@ message PollInit {
|
|||||||
bytes signature = 8 // signature of all above fields
|
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
|
||||||
|
}
|
||||||
|
```
|
@ -5,13 +5,15 @@ import { PollType } from './types/PollType'
|
|||||||
import { BigNumber, Wallet } from 'ethers'
|
import { BigNumber, Wallet } from 'ethers'
|
||||||
import PollInit from './utils/proto/PollInit'
|
import PollInit from './utils/proto/PollInit'
|
||||||
import { WakuMessage } from 'js-waku'
|
import { WakuMessage } from 'js-waku'
|
||||||
|
import { TimedPollVoteMsg } from './models/TimedPollVoteMsg'
|
||||||
|
import TimedPollVote from './utils/proto/TimedPollVote'
|
||||||
|
|
||||||
class WakuVoting {
|
class WakuVoting {
|
||||||
private appName: string
|
private appName: string
|
||||||
private waku: Waku | undefined
|
private waku: Waku | undefined
|
||||||
public tokenAddress: string
|
public tokenAddress: string
|
||||||
private pollInitTopic: string
|
private pollInitTopic: string
|
||||||
|
private timedPollVoteTopic: string
|
||||||
private static async createWaku() {
|
private static async createWaku() {
|
||||||
const waku = await Waku.create()
|
const waku = await Waku.create()
|
||||||
const nodes = await getStatusFleetNodes()
|
const nodes = await getStatusFleetNodes()
|
||||||
@ -29,6 +31,7 @@ class WakuVoting {
|
|||||||
this.appName = appName
|
this.appName = appName
|
||||||
this.tokenAddress = tokenAddress
|
this.tokenAddress = tokenAddress
|
||||||
this.pollInitTopic = `/${this.appName}/waku-polling/timed-polls-init/proto/`
|
this.pollInitTopic = `/${this.appName}/waku-polling/timed-polls-init/proto/`
|
||||||
|
this.timedPollVoteTopic = `/${this.appName}/waku-polling/votes/proto/`
|
||||||
this.waku = waku
|
this.waku = waku
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,6 +67,30 @@ class WakuVoting {
|
|||||||
.map((msg) => PollInit.decode(msg.payload, msg.timestamp))
|
.map((msg) => PollInit.decode(msg.payload, msg.timestamp))
|
||||||
.filter((poll): poll is PollInitMsg => !!poll)
|
.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
|
export default WakuVoting
|
||||||
|
74
packages/core/src/models/TimedPollVoteMsg.ts
Normal file
74
packages/core/src/models/TimedPollVoteMsg.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
50
packages/core/src/utils/proto/TimedPollVote.ts
Normal file
50
packages/core/src/utils/proto/TimedPollVote.ts
Normal 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 }
|
49
packages/core/test/models/TimedPollVoteMsg.test.ts
Normal file
49
packages/core/test/models/TimedPollVoteMsg.test.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
43
packages/core/test/utils/proto/TimedPollVote.test.ts
Normal file
43
packages/core/test/utils/proto/TimedPollVote.test.ts
Normal 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),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
13
packages/core/types/protons/index.d.ts
vendored
13
packages/core/types/protons/index.d.ts
vendored
@ -14,11 +14,24 @@ declare module 'protons' {
|
|||||||
signature: Uint8Array
|
signature: Uint8Array
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TimedPollVote = {
|
||||||
|
id: Uint8Array
|
||||||
|
voter: Uint8Array
|
||||||
|
timestamp: number
|
||||||
|
answer: number
|
||||||
|
tokenAmount?: Uint8Array
|
||||||
|
signature: Uint8Array
|
||||||
|
}
|
||||||
|
|
||||||
function protons(init: string): {
|
function protons(init: string): {
|
||||||
PollInit: {
|
PollInit: {
|
||||||
encode: (pollInit: PollInit) => Uint8Array,
|
encode: (pollInit: PollInit) => Uint8Array,
|
||||||
decode: (payload: Uint8Array) => PollInit
|
decode: (payload: Uint8Array) => PollInit
|
||||||
}
|
}
|
||||||
|
TimedPollVote:{
|
||||||
|
encode: (timedPollVote: TimedPollVote) => Uint8Array,
|
||||||
|
decode: (payload: Uint8Array) => TimedPollVote
|
||||||
|
}
|
||||||
}
|
}
|
||||||
export = protons
|
export = protons
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user