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,
"sourceMaps": true,
"env": {
"DEBUG": "*,-vite*,-connect:*",
// "DEBUG": "*,-vite*,-connect:*",
"DEBUG_HIDE_DATE": "0",
"DEBUG_COLORS": "1",
"VITE_NODE": "true"

View File

@ -28,7 +28,7 @@ export interface ClientOptions {
* Public key of a community to join.
*/
publicKey: string
environment?: 'test' // 'production' | 'test'
environment?: 'development' | 'preview' | 'production'
/**
* Custom storage for data persistance
* @default window.localStorage
@ -132,7 +132,7 @@ class Client {
}
static async start(options: ClientOptions): Promise<Client> {
const { environment = 'test' } = options
// const { environment = 'development' } = options
let waku: LightNode | undefined
let client: Client | undefined
@ -161,7 +161,7 @@ class Client {
* >@see https://forum.vac.dev/t/waku-v2-scalability-studies/142/2
*/
bootstrap({
list: peers[environment],
list: peers['production'],
timeout: 0,
// note: Infinity prevents connection
// 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
*/
const production = [
'/dns4/boot-01.do-ams3.shards.test.status.im/tcp/443/wss/p2p/16Uiu2HAmAR24Mbb6VuzoyUiGx42UenDkshENVDj4qnmmbabLvo31',
'/dns4/boot-01.gc-us-central1-a.shards.test.status.im/tcp/443/wss/p2p/16Uiu2HAm8mUZ18tBWPXDQsaF7PbCKYA35z7WB2xNZH2EVq1qS8LJ',
'/dns4/boot-01.ac-cn-hongkong-c.shards.test.status.im/tcp/443/wss/p2p/16Uiu2HAmGwcE8v7gmJNEWFtZtojYpPMTHy2jBLL6xRk33qgDxFWX',
'/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-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-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',
]
// 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 = {
// production: [],
test: [
'/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-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-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',
],
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 { ethers } from 'ethers'
import { publicKeyToETHAddress } from '../utils/public-key-to-eth-address'
export class EthereumClient {
private provider: ethers.JsonRpcApiProvider
#provider: ethers.JsonRpcApiProvider
constructor(url: string) {
this.provider = new ethers.JsonRpcProvider(url)
this.#provider = new ethers.JsonRpcProvider(url)
}
stop() {
this.#provider.destroy()
}
async resolvePublicKey(
@ -13,7 +19,7 @@ export class EthereumClient {
options: { compress: boolean }
): Promise<string | undefined> {
try {
const resolver = await this.provider.getResolver(ensName)
const resolver = await this.#provider.getResolver(ensName)
if (!resolver) {
return
@ -24,7 +30,7 @@ export class EthereumClient {
'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 [x, y] = await resolverContract.pubkey(node)
@ -42,4 +48,31 @@ export class EthereumClient {
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 { isEncrypted } from '../client/community/is-encrypted'
import { contracts } from '../consts/contracts'
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 {
ApplicationMetadataMessage,
ApplicationMetadataMessage_Type,
} from '../protos/application-metadata-message_pb'
import {
CommunityDescription,
// CommunityTokenPermission_Type,
CommunityTokenPermission_Type,
} from '../protos/communities_pb'
import { ProtocolMessage } from '../protos/protocol-message_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 { isClockValid } from '../utils/is-clock-valid'
import { payloadToId } from '../utils/payload-to-id'
// import { publicKeyToETHAddress } from '../utils/public-key-to-eth-address'
import { recoverPublicKey } from '../utils/recover-public-key'
import { mapChannel } from './map-channel'
import { mapCommunity } from './map-community'
@ -35,7 +36,8 @@ import type { UserInfo } from './map-user'
import type { LightNode } from '@waku/interfaces'
export interface RequestClientOptions {
environment?: 'test' // 'production' | 'test'
ethProviderApiKey: string
// environment?: 'development' | 'preview' | 'production'
}
class RequestClient {
@ -43,16 +45,34 @@ class RequestClient {
/** Cache. */
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.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> {
const { environment = 'test' } = options
// const { environment = 'development' } = options
let waku: LightNode | undefined
let client: RequestClient | undefined
@ -69,7 +89,7 @@ class RequestClient {
libp2p: {
peerDiscovery: [
bootstrap({
list: peers[environment],
list: peers['production'],
timeout: 0,
// note: Infinity prevents connection
// tagTTL: Infinity,
@ -84,8 +104,12 @@ class RequestClient {
await waku.start()
await waitForRemotePeer(waku, [Protocols.Store], 10 * 1000)
const started = true
client = new RequestClient(waku, started)
client = new RequestClient(waku, {
started: true,
ethProviderURLs: providers['production'].infura,
ethProviderApiKey: options.ethProviderApiKey,
contractAddresses: contracts['production'],
})
} catch (error) {
if (waku) {
await waku.stop()
@ -98,13 +122,36 @@ class RequestClient {
}
public async stop() {
if (!this.started) {
if (!this.#started) {
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 (
@ -153,13 +200,14 @@ class RequestClient {
public fetchCommunityDescription = async (
/** Compressed */
publicKey: string
communityPublicKey: string
): Promise<CommunityDescription | undefined> => {
const contentTopic = idToContentTopic(publicKey)
const symmetricKey = await generateKeyFromPassword(publicKey)
const contentTopic = idToContentTopic(communityPublicKey)
const symmetricKey = await generateKeyFromPassword(communityPublicKey)
let communityDescription: CommunityDescription | undefined = undefined
try {
// todo: use queryGenerator() instead
await this.waku.store.queryWithOrderedCallback(
[
createDecoder(contentTopic, symmetricKey, {
@ -167,7 +215,7 @@ class RequestClient {
shard: 32,
}),
],
wakuMessage => {
async wakuMessage => {
// handle
const message = this.handleWakuMessage(wakuMessage)
@ -197,41 +245,48 @@ class RequestClient {
return
}
const signerPublicKey = `0x${compressPublicKey(
message.signerPublicKey
)}`
// isSignatureValid
if (isEncrypted(decodedCommunityDescription.tokenPermissions)) {
// const permission = Object.values(
// decodedCommunityDescription.tokenPermissions
// ).find(
// permission =>
// permission.type ===
// CommunityTokenPermission_Type.BECOME_TOKEN_OWNER
// )
// if (!permission) {
// return
// }
// const criteria = permission.tokenCriteria[0]
// const contracts = criteria?.contractAddresses
// const chainId = Object.keys(contracts)[0]
// if (!chainId) {
// return
// }
// // get client config based on chainId
// // get client
// const client = new EthereumClient(
// `https://mainnet.infura.io/v3/${process.env.KEY}`
// )
// // call status contract for chainId
// const address = publicKeyToETHAddress(publicKey)
// // call contracts from previous call with address
// const ownerPublicKey = '0x0'
// if (ownerPublicKey !== signerPublicKey) {
// return
// }
} else if (publicKey !== signerPublicKey) {
// todo?: zod
const permission = Object.values(
decodedCommunityDescription.tokenPermissions
).find(
permission =>
permission.type ===
CommunityTokenPermission_Type.BECOME_TOKEN_OWNER
)
if (!permission) {
return
}
const criteria = permission.tokenCriteria[0]
const contracts = criteria?.contractAddresses
const chainId = Object.keys(contracts)[0]
if (!chainId) {
return
}
const ethereumClient = this.getEthereumClient(Number(chainId))
if (!ethereumClient) {
return
}
const ownerPublicKey = await ethereumClient.resolveOwner(
this.#contractAddresses[Number(chainId)]
.CommunityOwnerTokenRegistry,
communityPublicKey
)
if (ownerPublicKey !== message.signerPublicKey) {
return
}
} else if (
communityPublicKey !==
`0x${compressPublicKey(message.signerPublicKey)}`
) {
return
}