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:
parent
4e3f3bb644
commit
195e3f9a5a
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@status-im/js': minor
|
||||
---
|
||||
|
||||
validate metadata for communities with owner token
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue