diff --git a/.changeset/flat-rabbits-promise.md b/.changeset/flat-rabbits-promise.md new file mode 100644 index 00000000..c535cc3e --- /dev/null +++ b/.changeset/flat-rabbits-promise.md @@ -0,0 +1,5 @@ +--- +'@status-im/js': minor +--- + +validate metadata for communities with owner token diff --git a/.vscode/launch.json b/.vscode/launch.json index 3eabe96f..5680f443 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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" diff --git a/packages/status-js/src/client/client.ts b/packages/status-js/src/client/client.ts index df5bd495..00b6b196 100644 --- a/packages/status-js/src/client/client.ts +++ b/packages/status-js/src/client/client.ts @@ -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 { - 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, diff --git a/packages/status-js/src/consts/contracts.ts b/packages/status-js/src/consts/contracts.ts new file mode 100644 index 00000000..b286ce29 --- /dev/null +++ b/packages/status-js/src/consts/contracts.ts @@ -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, +} diff --git a/packages/status-js/src/consts/peers.ts b/packages/status-js/src/consts/peers.ts index 7bc66b9e..39bbfe89 100644 --- a/packages/status-js/src/consts/peers.ts +++ b/packages/status-js/src/consts/peers.ts @@ -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, } diff --git a/packages/status-js/src/consts/providers.ts b/packages/status-js/src/consts/providers.ts new file mode 100644 index 00000000..bc0ec73d --- /dev/null +++ b/packages/status-js/src/consts/providers.ts @@ -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> +> = { + development: production, + preview: production, + production, +} diff --git a/packages/status-js/src/ethereum-client/ethereum-client.ts b/packages/status-js/src/ethereum-client/ethereum-client.ts index 0eea7d35..905bcfaa 100644 --- a/packages/status-js/src/ethereum-client/ethereum-client.ts +++ b/packages/status-js/src/ethereum-client/ethereum-client.ts @@ -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 { 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 { + 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 + } + } } diff --git a/packages/status-js/src/request-client/request-client.ts b/packages/status-js/src/request-client/request-client.ts index 2f246fce..302c166a 100644 --- a/packages/status-js/src/request-client/request-client.ts +++ b/packages/status-js/src/request-client/request-client.ts @@ -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 - private started: boolean + #started: boolean - constructor(waku: LightNode, started = false) { + #ethProviderURLs: Record + #ethProviderApiKey: string + #ethereumClients: Map + + #contractAddresses: Record> + + constructor( + waku: LightNode, + options: { + ethProviderURLs: Record + ethProviderApiKey: string + contractAddresses: Record> + 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 { - 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 => { - 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 }