mirror of
https://github.com/status-im/wakuconnect-vote-poll-sdk.git
synced 2025-02-23 23:58:14 +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)
|
||||
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
|
||||
}
|
||||
```
|
@ -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
|
||||
|
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user