Use EIP712 for feature (#188)

This commit is contained in:
Szymon Szlachtowicz 2021-08-31 15:46:55 +02:00 committed by GitHub
parent 343cd57fee
commit 5c38980d4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 106 additions and 44 deletions

View File

@ -33,8 +33,8 @@ function getTop(map: CommunitiesFeatureVotes, top: number) {
.slice(0, top)
}
export async function receiveWakuFeature(waku: Waku | undefined, topic: string) {
let messages = await receiveWakuFeatureMsg(waku, topic)
export async function receiveWakuFeature(waku: Waku | undefined, topic: string, chainId: number) {
let messages = await receiveWakuFeatureMsg(waku, topic, chainId)
const wakuFeatured: CommunitiesFeatureVotes = {}
let top5: [string, CommunityFeatureVotes][] = []
if (messages && messages?.length > 0) {

View File

@ -1,22 +1,56 @@
import { Waku, WakuMessage } from 'js-waku'
import { WakuFeatureData } from '../models/waku'
import { packAndArrayify } from './ethMessage'
import { recoverAddress } from './ethMessage'
import { utils, BigNumber } from 'ethers'
import { JsonRpcSigner } from '@ethersproject/providers'
import proto from './loadProtons'
import { TypedFeature } from '../models/TypedData'
function verifyWakuFeatureMsg(data: WakuFeatureData | undefined): data is WakuFeatureData {
function createTypedData(chainId: number, voter: string, sntAmount: BigNumber, publicKey: string, timestamp: Date) {
return {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
],
Feature: [
{ name: 'voter', type: 'address' },
{ name: 'sntAmount', type: 'uint256' },
{ name: 'publicKey', type: 'bytes' },
{ name: 'timestamp', type: 'uint64' },
{ name: 'chainId', type: 'uint64' },
],
},
primaryType: 'Feature',
domain: {
name: 'Featuring vote',
version: '1',
chainId: chainId,
},
message: {
voter: voter,
sntAmount: sntAmount.toHexString(),
publicKey: publicKey,
timestamp: timestamp.getTime(),
},
} as TypedFeature
}
function verifyWakuFeatureMsg(data: WakuFeatureData | undefined, chainId: number): data is WakuFeatureData {
if (!data) {
return false
}
const types = ['address', 'uint256', 'address', 'uint256']
const message = [data.voter, data.sntAmount, data.publicKey, data.timestamp.getTime()]
const verifiedAddress = utils.verifyMessage(packAndArrayify(types, message), data.sign)
if (data.msgTimestamp?.getTime() != data.timestamp.getTime() || verifiedAddress != data.voter) {
const typedData = createTypedData(chainId, data.voter, data.sntAmount, data.publicKey, data.timestamp)
try {
const verifiedAddress = utils.getAddress(recoverAddress(typedData, data.sign))
if (data.msgTimestamp?.getTime() != data.timestamp.getTime() || verifiedAddress != data.voter) {
return false
}
return true
} catch {
return false
}
return true
}
function decodeWakuFeature(msg: WakuMessage): WakuFeatureData | undefined {
@ -40,14 +74,14 @@ function decodeWakuFeature(msg: WakuMessage): WakuFeatureData | undefined {
}
}
export function decodeWakuFeatures(messages: WakuMessage[] | null) {
return messages?.map(decodeWakuFeature).filter(verifyWakuFeatureMsg)
export function decodeWakuFeatures(messages: WakuMessage[] | null, chainId: number) {
return messages?.map(decodeWakuFeature).filter((e): e is WakuFeatureData => verifyWakuFeatureMsg(e, chainId))
}
export async function receiveWakuFeatureMsg(waku: Waku | undefined, topic: string) {
export async function receiveWakuFeatureMsg(waku: Waku | undefined, topic: string, chainId: number) {
if (waku) {
const messages = await waku.store.queryHistory({ contentTopics: [topic] })
return decodeWakuFeatures(messages)
return decodeWakuFeatures(messages, chainId)
}
}
@ -56,29 +90,30 @@ export async function createWakuFeatureMsg(
signer: JsonRpcSigner | undefined,
sntAmount: BigNumber,
publicKey: string,
contentTopic: string
contentTopic: string,
chainId: number,
sig?: string,
time?: Date
) {
if (!account || !signer) {
return undefined
}
const provider = signer.provider
const signerAddress = await signer?.getAddress()
if (signerAddress != account) {
return undefined
}
const timestamp = new Date()
const timestamp = time ? time : new Date()
const data = createTypedData(chainId, account, sntAmount, publicKey, timestamp)
const signature = sig ? sig : await provider?.send('eth_signTypedData_v3', [account, JSON.stringify(data)])
const types = ['address', 'uint256', 'address', 'uint256']
const message = [account, sntAmount, publicKey, BigNumber.from(timestamp.getTime())]
const sign = await signer.signMessage(packAndArrayify(types, message))
if (sign) {
if (signature) {
const payload = proto.WakuFeature.encode({
voter: account,
sntAmount: utils.arrayify(sntAmount),
publicKey,
timestamp,
sign,
sign: signature,
})
const msg = WakuMessage.fromBytes(payload, {

View File

@ -7,17 +7,21 @@ import { BigNumber } from 'ethers'
export function useSendWakuFeature() {
const { waku } = useWaku()
const { account, library } = useEthers()
const { account, library, chainId } = useEthers()
const { config } = useConfig()
const sendWakuFeature = useCallback(
async (voteAmount: number, publicKey: string) => {
if (!chainId) {
return
}
const msg = await createWakuFeatureMsg(
account,
library?.getSigner(),
BigNumber.from(voteAmount),
publicKey,
config.wakuFeatureTopic
config.wakuFeatureTopic,
chainId
)
if (msg) {
if (waku) {
@ -27,7 +31,7 @@ export function useSendWakuFeature() {
}
}
},
[waku, library, account]
[waku, library, account, chainId]
)
return sendWakuFeature

View File

@ -12,3 +12,18 @@ export interface TypedVote extends TypedMessage<VoteType> {
voter: string
}
}
type FeatureType = {
EIP712Domain: { name: string; type: string }[]
Feature: { name: string; type: string }[]
}
export interface TypedFeature extends TypedMessage<FeatureType> {
message: {
voter: string
sntAmount: string
publicKey: string
timestamp: number
chainId: number
}
}

View File

@ -1,3 +1,4 @@
import { useEthers } from '@usedapp/core'
import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react'
import { receiveWakuFeature } from '../../helpers/receiveWakuFeature'
import { useConfig } from '../config'
@ -24,16 +25,19 @@ export function WakuFeatureProvider({ children }: WakuFeatureProviderProps) {
const [featured, setFeatured] = useState<any[]>([])
const { waku } = useWaku()
const { config } = useConfig()
const { chainId } = useEthers()
useEffect(() => {
const get = async () => {
const { wakuFeatured, top5 } = await receiveWakuFeature(waku, config.wakuFeatureTopic)
setFeatureVotes(wakuFeatured)
setFeatured(top5)
if (chainId) {
const { wakuFeatured, top5 } = await receiveWakuFeature(waku, config.wakuFeatureTopic, chainId)
setFeatureVotes(wakuFeatured)
setFeatured(top5)
}
}
get()
const task = setInterval(get, 10000)
return () => clearInterval(task)
}, [waku?.libp2p?.peerId?.toString()])
}, [waku?.libp2p?.peerId?.toString(), chainId])
return <WakuFeatureContext.Provider value={{ featureVotes, featured }} children={children} />
}

View File

@ -99,28 +99,31 @@ describe('wakuMessage', () => {
})
const msg = await WakuMessage.fromBytes(payload)
const msg2 = await createWakuFeatureMsg(
alice.address,
'0x17ec8597ff92C3F44523bDc65BF0f1bE632917ff',
alice as unknown as JsonRpcSigner,
BigNumber.from(123),
bob.address,
'/test/'
BigNumber.from('0x10'),
'0x1234',
'/test/',
1337,
'0x4e744a50da20c547a4adf888d2bc411efcf3b833cfc79c461b340e6145c34dc314d7a1a82127fff63a34efec0ff11be48929bb6a020ed30dfdeab8ac8c32fffa1c',
new Date(1)
)
expect(msg2).to.not.be.undefined
if (msg2) {
const response = decodeWakuFeatures([msg, msg2]) ?? []
const response = decodeWakuFeatures([msg, msg2], 1337) ?? []
expect(response.length).to.eq(1)
const data = response[0]
expect(data).to.not.be.undefined
expect(data?.voter).to.eq(alice.address)
expect(data?.publicKey).to.eq(bob.address)
expect(data?.sntAmount).to.deep.eq(BigNumber.from(123))
expect(data?.voter).to.eq('0x17ec8597ff92C3F44523bDc65BF0f1bE632917ff')
expect(data?.publicKey).to.eq('0x1234')
expect(data?.sntAmount).to.deep.eq(BigNumber.from('0x10'))
}
})
it('empty', async () => {
expect(decodeWakuFeatures([])).to.deep.eq([])
expect(decodeWakuFeatures([], 1337)).to.deep.eq([])
})
})

View File

@ -132,12 +132,13 @@ async function fixture([alice, firstAddress, secondAddress]: any[], provider: Mo
return { contract, directory, alice, firstAddress, secondAddress, provider }
}
before(async () => {
const { contract } = await loadFixture(fixture)
typedData.domain.chainId = 1
typedData.domain.verifyingContract = contract.address
})
describe('Contract', () => {
before(async () => {
const { contract } = await loadFixture(fixture)
typedData.domain.chainId = 1
typedData.domain.verifyingContract = contract.address
})
it('deploys properly', async () => {
const { contract, directory } = await loadFixture(fixture)
await expect(await contract.directory()).to.eq(directory.address)