validate metadata for communities with owner token (#541)

* u

* u

* u

* a

* a

* u

* a

* u

* a

* f

* u

* c
This commit is contained in:
Felicio Mununga 2024-03-26 13:45:48 +09:00 committed by GitHub
parent 4e3f3bb644
commit 195e3f9a5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 236 additions and 68 deletions

View File

@ -0,0 +1,5 @@
---
'@status-im/js': minor
---
validate metadata for communities with owner token

2
.vscode/launch.json vendored
View File

@ -26,7 +26,7 @@
"smartStep": true, "smartStep": true,
"sourceMaps": true, "sourceMaps": true,
"env": { "env": {
"DEBUG": "*,-vite*,-connect:*", // "DEBUG": "*,-vite*,-connect:*",
"DEBUG_HIDE_DATE": "0", "DEBUG_HIDE_DATE": "0",
"DEBUG_COLORS": "1", "DEBUG_COLORS": "1",
"VITE_NODE": "true" "VITE_NODE": "true"

View File

@ -28,7 +28,7 @@ export interface ClientOptions {
* Public key of a community to join. * Public key of a community to join.
*/ */
publicKey: string publicKey: string
environment?: 'test' // 'production' | 'test' environment?: 'development' | 'preview' | 'production'
/** /**
* Custom storage for data persistance * Custom storage for data persistance
* @default window.localStorage * @default window.localStorage
@ -132,7 +132,7 @@ class Client {
} }
static async start(options: ClientOptions): Promise<Client> { static async start(options: ClientOptions): Promise<Client> {
const { environment = 'test' } = options // const { environment = 'development' } = options
let waku: LightNode | undefined let waku: LightNode | undefined
let client: Client | undefined let client: Client | undefined
@ -161,7 +161,7 @@ class Client {
* >@see https://forum.vac.dev/t/waku-v2-scalability-studies/142/2 * >@see https://forum.vac.dev/t/waku-v2-scalability-studies/142/2
*/ */
bootstrap({ bootstrap({
list: peers[environment], list: peers['production'],
timeout: 0, timeout: 0,
// note: Infinity prevents connection // note: Infinity prevents connection
// tagTTL: Infinity, // tagTTL: Infinity,

View File

@ -0,0 +1,42 @@
/**
* source: @see https://github.com/status-im/communities-contracts#deployments
* source: @see https://github.com/status-im/community-dapp/tree/master/packages/contracts#deployments
*/
// const development = {
// 5: {
// CommunityOwnerTokenRegistry: '0x59510D0b235c75d7bCAEb66A420e9bb0edC976AE',
// },
// 11155111: {
// CommunityOwnerTokenRegistry: '0x98E0A38A9c198F9F49a4F6b49475aE0c92aBbB66',
// },
// 420: {
// CommunityOwnerTokenRegistry: '0x99F0Eeb7E9F1Da6CA9DDf77dD7810B665FD85750',
// },
// // 11155420: {
// // CommunityOwnerTokenRegistry: '0xfFa8A255D905c909379859eA45B959D090DDC2d4',
// // },
// // 421613: {
// // CommunityOwnerTokenRegistry: '0x9C84f9f9970B22E67f1B2BE46ABb1C09741FF7d7',
// // },
// 421614: {
// CommunityOwnerTokenRegistry: '0x9C84f9f9970B22E67f1B2BE46ABb1C09741FF7d7',
// },
// }
const production = {
1: {
CommunityOwnerTokenRegistry: '0x898331B756EE1f29302DeF227a4471e960c50612',
},
10: {
CommunityOwnerTokenRegistry: '0x0AF2c7d60E89a941D586216059814D1Cb4Bd4CAb',
},
42161: {
CommunityOwnerTokenRegistry: '0x76352764590378011CAE677b50110Ae02eDE2b62',
},
}
export const contracts = {
development: production,
preview: production,
production,
}

View File

@ -2,17 +2,23 @@
* source: @see https://fleets.status.im * source: @see https://fleets.status.im
*/ */
// note!: users may experience additional latency due to cross-regional connection const production = [
// todo: use "dynamic" discovery protocol instead '/dns4/boot-01.do-ams3.shards.test.status.im/tcp/443/wss/p2p/16Uiu2HAmAR24Mbb6VuzoyUiGx42UenDkshENVDj4qnmmbabLvo31',
// todo?: use a regional map together with an environment variable for the peer selection (e.g. `VERCEL_REGION`, but probably limited to Serverless Functions) '/dns4/boot-01.gc-us-central1-a.shards.test.status.im/tcp/443/wss/p2p/16Uiu2HAm8mUZ18tBWPXDQsaF7PbCKYA35z7WB2xNZH2EVq1qS8LJ',
export const peers = { '/dns4/boot-01.ac-cn-hongkong-c.shards.test.status.im/tcp/443/wss/p2p/16Uiu2HAmGwcE8v7gmJNEWFtZtojYpPMTHy2jBLL6xRk33qgDxFWX',
// production: [],
test: [
'/dns4/store-01.do-ams3.shards.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAmAUdrQ3uwzuE4Gy4D56hX6uLKEeerJAnhKEHZ3DxF1EfT', '/dns4/store-01.do-ams3.shards.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAmAUdrQ3uwzuE4Gy4D56hX6uLKEeerJAnhKEHZ3DxF1EfT',
'/dns4/store-02.do-ams3.shards.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAm9aDJPkhGxc2SFcEACTFdZ91Q5TJjp76qZEhq9iF59x7R', '/dns4/store-02.do-ams3.shards.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAm9aDJPkhGxc2SFcEACTFdZ91Q5TJjp76qZEhq9iF59x7R',
'/dns4/store-01.gc-us-central1-a.shards.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAmMELCo218hncCtTvC2Dwbej3rbyHQcR8erXNnKGei7WPZ', '/dns4/store-01.gc-us-central1-a.shards.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAmMELCo218hncCtTvC2Dwbej3rbyHQcR8erXNnKGei7WPZ',
'/dns4/store-02.gc-us-central1-a.shards.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAmJnVR7ZzFaYvciPVafUXuYGLHPzSUigqAmeNw9nJUVGeM', '/dns4/store-02.gc-us-central1-a.shards.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAmJnVR7ZzFaYvciPVafUXuYGLHPzSUigqAmeNw9nJUVGeM',
'/dns4/store-01.ac-cn-hongkong-c.shards.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAm2M7xs7cLPc3jamawkEqbr7cUJX11uvY7LxQ6WFUdUKUT', '/dns4/store-01.ac-cn-hongkong-c.shards.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAm2M7xs7cLPc3jamawkEqbr7cUJX11uvY7LxQ6WFUdUKUT',
'/dns4/store-02.ac-cn-hongkong-c.shards.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAm9CQhsuwPR54q27kNj9iaQVfyRzTGKrhFmr94oD8ujU6P', '/dns4/store-02.ac-cn-hongkong-c.shards.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAm9CQhsuwPR54q27kNj9iaQVfyRzTGKrhFmr94oD8ujU6P',
], ]
// note!: users may experience additional latency due to cross-regional connection
// todo: use "dynamic" discovery protocol instead
// todo?: use a regional map together with an environment variable for the peer selection (e.g. `VERCEL_REGION`, but probably limited to Serverless Functions)
export const peers = {
development: production,
preview: production,
production,
} }

View File

@ -0,0 +1,27 @@
// const development = {
// infura: {
// 5: 'https://goerli.infura.io/v3/',
// 11155111: 'https://sepolia.infura.io/v3/',
// 420: 'https://optimism-sepolia.infura.io/v3',
// // 11155420: 'https://optimism-goerli.infura.io/v3'
// // 421613: 'https://arbitrum-goerli.infura.io/v3/',
// 421614: 'https://arbitrum-sepolia.infura.io/v3/',
// },
// }
const production = {
infura: {
1: 'https://mainnet.infura.io/v3/',
10: 'https://optimism-mainnet.infura.io/v3/',
42161: 'https://arbitrum-mainnet.infura.io/v3/',
},
}
export const providers: Record<
'development' | 'preview' | 'production',
Record<string, Record<number, string>>
> = {
development: production,
preview: production,
production,
}

View File

@ -1,11 +1,17 @@
import { Point } from 'ethereum-cryptography/secp256k1' import { Point } from 'ethereum-cryptography/secp256k1'
import { ethers } from 'ethers' import { ethers } from 'ethers'
import { publicKeyToETHAddress } from '../utils/public-key-to-eth-address'
export class EthereumClient { export class EthereumClient {
private provider: ethers.JsonRpcApiProvider #provider: ethers.JsonRpcApiProvider
constructor(url: string) { constructor(url: string) {
this.provider = new ethers.JsonRpcProvider(url) this.#provider = new ethers.JsonRpcProvider(url)
}
stop() {
this.#provider.destroy()
} }
async resolvePublicKey( async resolvePublicKey(
@ -13,7 +19,7 @@ export class EthereumClient {
options: { compress: boolean } options: { compress: boolean }
): Promise<string | undefined> { ): Promise<string | undefined> {
try { try {
const resolver = await this.provider.getResolver(ensName) const resolver = await this.#provider.getResolver(ensName)
if (!resolver) { if (!resolver) {
return return
@ -24,7 +30,7 @@ export class EthereumClient {
'function pubkey(bytes32 node) view returns (bytes32 x, bytes32 y)', 'function pubkey(bytes32 node) view returns (bytes32 x, bytes32 y)',
] ]
const resolverContract = new ethers.Contract(address, abi, this.provider) const resolverContract = new ethers.Contract(address, abi, this.#provider)
const node = ethers.namehash(ensName) const node = ethers.namehash(ensName)
const [x, y] = await resolverContract.pubkey(node) const [x, y] = await resolverContract.pubkey(node)
@ -42,4 +48,31 @@ export class EthereumClient {
return return
} }
} }
async resolveOwner(
registryContractAddress: string,
communityPublicKey: string
): Promise<string | undefined> {
try {
const registryContract = new ethers.Contract(
registryContractAddress,
['function getEntry(address _communityAddress) view returns (address)'],
this.#provider
)
const ownerContractAddress = await registryContract.getEntry(
publicKeyToETHAddress(communityPublicKey)
)
const ownerContract = new ethers.Contract(
ownerContractAddress,
['function signerPublicKey() view returns (bytes)'],
this.#provider
)
const owner = await ownerContract.signerPublicKey()
return owner
} catch {
return
}
}
} }

View File

@ -5,15 +5,17 @@ import { createLightNode, waitForRemotePeer } from '@waku/sdk'
import { bytesToHex } from 'ethereum-cryptography/utils' import { bytesToHex } from 'ethereum-cryptography/utils'
import { isEncrypted } from '../client/community/is-encrypted' import { isEncrypted } from '../client/community/is-encrypted'
import { contracts } from '../consts/contracts'
import { peers } from '../consts/peers' import { peers } from '../consts/peers'
// import { EthereumClient } from '../ethereum-client/ethereum-client' import { providers } from '../consts/providers'
import { EthereumClient } from '../ethereum-client/ethereum-client'
import { import {
ApplicationMetadataMessage, ApplicationMetadataMessage,
ApplicationMetadataMessage_Type, ApplicationMetadataMessage_Type,
} from '../protos/application-metadata-message_pb' } from '../protos/application-metadata-message_pb'
import { import {
CommunityDescription, CommunityDescription,
// CommunityTokenPermission_Type, CommunityTokenPermission_Type,
} from '../protos/communities_pb' } from '../protos/communities_pb'
import { ProtocolMessage } from '../protos/protocol-message_pb' import { ProtocolMessage } from '../protos/protocol-message_pb'
import { ContactCodeAdvertisement } from '../protos/push-notifications_pb' import { ContactCodeAdvertisement } from '../protos/push-notifications_pb'
@ -22,7 +24,6 @@ import { generateKeyFromPassword } from '../utils/generate-key-from-password'
import { idToContentTopic } from '../utils/id-to-content-topic' import { idToContentTopic } from '../utils/id-to-content-topic'
import { isClockValid } from '../utils/is-clock-valid' import { isClockValid } from '../utils/is-clock-valid'
import { payloadToId } from '../utils/payload-to-id' import { payloadToId } from '../utils/payload-to-id'
// import { publicKeyToETHAddress } from '../utils/public-key-to-eth-address'
import { recoverPublicKey } from '../utils/recover-public-key' import { recoverPublicKey } from '../utils/recover-public-key'
import { mapChannel } from './map-channel' import { mapChannel } from './map-channel'
import { mapCommunity } from './map-community' import { mapCommunity } from './map-community'
@ -35,7 +36,8 @@ import type { UserInfo } from './map-user'
import type { LightNode } from '@waku/interfaces' import type { LightNode } from '@waku/interfaces'
export interface RequestClientOptions { export interface RequestClientOptions {
environment?: 'test' // 'production' | 'test' ethProviderApiKey: string
// environment?: 'development' | 'preview' | 'production'
} }
class RequestClient { class RequestClient {
@ -43,16 +45,34 @@ class RequestClient {
/** Cache. */ /** Cache. */
public readonly wakuMessages: Set<string> public readonly wakuMessages: Set<string>
private started: boolean #started: boolean
constructor(waku: LightNode, started = false) { #ethProviderURLs: Record<number, string>
#ethProviderApiKey: string
#ethereumClients: Map<number, EthereumClient>
#contractAddresses: Record<number, Record<string, string>>
constructor(
waku: LightNode,
options: {
ethProviderURLs: Record<number, string>
ethProviderApiKey: string
contractAddresses: Record<number, Record<string, string>>
started?: boolean
}
) {
this.waku = waku this.waku = waku
this.wakuMessages = new Set() this.wakuMessages = new Set()
this.started = started this.#started = options.started ?? false
this.#ethProviderURLs = options.ethProviderURLs
this.#ethProviderApiKey = options.ethProviderApiKey
this.#ethereumClients = new Map()
this.#contractAddresses = options.contractAddresses
} }
static async start(options: RequestClientOptions): Promise<RequestClient> { static async start(options: RequestClientOptions): Promise<RequestClient> {
const { environment = 'test' } = options // const { environment = 'development' } = options
let waku: LightNode | undefined let waku: LightNode | undefined
let client: RequestClient | undefined let client: RequestClient | undefined
@ -69,7 +89,7 @@ class RequestClient {
libp2p: { libp2p: {
peerDiscovery: [ peerDiscovery: [
bootstrap({ bootstrap({
list: peers[environment], list: peers['production'],
timeout: 0, timeout: 0,
// note: Infinity prevents connection // note: Infinity prevents connection
// tagTTL: Infinity, // tagTTL: Infinity,
@ -84,8 +104,12 @@ class RequestClient {
await waku.start() await waku.start()
await waitForRemotePeer(waku, [Protocols.Store], 10 * 1000) await waitForRemotePeer(waku, [Protocols.Store], 10 * 1000)
const started = true client = new RequestClient(waku, {
client = new RequestClient(waku, started) started: true,
ethProviderURLs: providers['production'].infura,
ethProviderApiKey: options.ethProviderApiKey,
contractAddresses: contracts['production'],
})
} catch (error) { } catch (error) {
if (waku) { if (waku) {
await waku.stop() await waku.stop()
@ -98,13 +122,36 @@ class RequestClient {
} }
public async stop() { public async stop() {
if (!this.started) { if (!this.#started) {
throw new Error('Waku instance not created by class initialization') throw new Error('Waku instance not created by class initialization')
} }
await this.waku.stop() await Promise.all([
async () => this.waku.stop(),
[...this.#ethereumClients.values()].map(async provider =>
provider.stop()
),
])
this.started = false this.#started = false
}
private getEthereumClient = (chainId: number): EthereumClient | undefined => {
const client = this.#ethereumClients.get(chainId)
if (!client) {
const url = this.#ethProviderURLs[chainId]
if (!url) {
return
}
const client = new EthereumClient(url + this.#ethProviderApiKey)
return this.#ethereumClients.set(chainId, client).get(chainId)
}
return client
} }
public fetchCommunity = async ( public fetchCommunity = async (
@ -153,13 +200,14 @@ class RequestClient {
public fetchCommunityDescription = async ( public fetchCommunityDescription = async (
/** Compressed */ /** Compressed */
publicKey: string communityPublicKey: string
): Promise<CommunityDescription | undefined> => { ): Promise<CommunityDescription | undefined> => {
const contentTopic = idToContentTopic(publicKey) const contentTopic = idToContentTopic(communityPublicKey)
const symmetricKey = await generateKeyFromPassword(publicKey) const symmetricKey = await generateKeyFromPassword(communityPublicKey)
let communityDescription: CommunityDescription | undefined = undefined let communityDescription: CommunityDescription | undefined = undefined
try { try {
// todo: use queryGenerator() instead
await this.waku.store.queryWithOrderedCallback( await this.waku.store.queryWithOrderedCallback(
[ [
createDecoder(contentTopic, symmetricKey, { createDecoder(contentTopic, symmetricKey, {
@ -167,7 +215,7 @@ class RequestClient {
shard: 32, shard: 32,
}), }),
], ],
wakuMessage => { async wakuMessage => {
// handle // handle
const message = this.handleWakuMessage(wakuMessage) const message = this.handleWakuMessage(wakuMessage)
@ -197,41 +245,48 @@ class RequestClient {
return return
} }
const signerPublicKey = `0x${compressPublicKey(
message.signerPublicKey
)}`
// isSignatureValid // isSignatureValid
if (isEncrypted(decodedCommunityDescription.tokenPermissions)) { if (isEncrypted(decodedCommunityDescription.tokenPermissions)) {
// const permission = Object.values( // todo?: zod
// decodedCommunityDescription.tokenPermissions const permission = Object.values(
// ).find( decodedCommunityDescription.tokenPermissions
// permission => ).find(
// permission.type === permission =>
// CommunityTokenPermission_Type.BECOME_TOKEN_OWNER permission.type ===
// ) CommunityTokenPermission_Type.BECOME_TOKEN_OWNER
// if (!permission) { )
// return
// } if (!permission) {
// const criteria = permission.tokenCriteria[0] return
// const contracts = criteria?.contractAddresses }
// const chainId = Object.keys(contracts)[0]
// if (!chainId) { const criteria = permission.tokenCriteria[0]
// return const contracts = criteria?.contractAddresses
// } const chainId = Object.keys(contracts)[0]
// // get client config based on chainId
// // get client if (!chainId) {
// const client = new EthereumClient( return
// `https://mainnet.infura.io/v3/${process.env.KEY}` }
// )
// // call status contract for chainId const ethereumClient = this.getEthereumClient(Number(chainId))
// const address = publicKeyToETHAddress(publicKey)
// // call contracts from previous call with address if (!ethereumClient) {
// const ownerPublicKey = '0x0' return
// if (ownerPublicKey !== signerPublicKey) { }
// return
// } const ownerPublicKey = await ethereumClient.resolveOwner(
} else if (publicKey !== signerPublicKey) { this.#contractAddresses[Number(chainId)]
.CommunityOwnerTokenRegistry,
communityPublicKey
)
if (ownerPublicKey !== message.signerPublicKey) {
return
}
} else if (
communityPublicKey !==
`0x${compressPublicKey(message.signerPublicKey)}`
) {
return return
} }