From 094e4d3069d16e5f1bcdf01dbd61bd78f3deefea Mon Sep 17 00:00:00 2001 From: Ry Racherbaumer Date: Tue, 24 Oct 2023 20:00:11 -0500 Subject: [PATCH] feat: add PPPP support --- package-lock.json | 14 +++- package.json | 3 +- src/Contacts.ts | 199 +++++++++++++++++++++++++++++++++++++++++++++ src/utils/topic.ts | 4 + 4 files changed, 215 insertions(+), 5 deletions(-) create mode 100644 src/Contacts.ts diff --git a/package-lock.json b/package-lock.json index 4f00959e3..87755ec76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "license": "MIT", "dependencies": { "@noble/secp256k1": "^1.5.2", - "@xmtp/proto": "^3.29.0", + "@xmtp/ecies-bindings-wasm": "^0.1.7", + "@xmtp/proto": "^3.31.2", "async-mutex": "^0.4.0", "elliptic": "^6.5.4", "ethers": "^5.5.3", @@ -4819,10 +4820,15 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@xmtp/ecies-bindings-wasm": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@xmtp/ecies-bindings-wasm/-/ecies-bindings-wasm-0.1.7.tgz", + "integrity": "sha512-+bwI5koXneyRLVUh9Mpm9Md7A1w8GdEKqPPEVhaszfWyGr1eSeMOnkLZ0JCXMxCirYJcmiC/aua96LiuAQpACQ==" + }, "node_modules/@xmtp/proto": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@xmtp/proto/-/proto-3.29.0.tgz", - "integrity": "sha512-+ibo+u6NwdzfLN3QEDMiNrnXd7eT1/+F2j5WWz3b4mk91wgn8lJ66fxFPwLTQs6AbaBBUmhO2cdpgIL/g4kvZg==", + "version": "3.31.2", + "resolved": "https://registry.npmjs.org/@xmtp/proto/-/proto-3.31.2.tgz", + "integrity": "sha512-A9CbaiOziqL7zzELUFMSknvhpqL0+fIWXI+Yi0S9IIRqESK18zWWPlYAa6oqYkAi8lBbtJnAZnD+thV+aXdCTg==", "dependencies": { "long": "^5.2.0", "protobufjs": "^7.0.0", diff --git a/package.json b/package.json index e9e8d860d..b56dd5863 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,8 @@ }, "dependencies": { "@noble/secp256k1": "^1.5.2", - "@xmtp/proto": "^3.29.0", + "@xmtp/ecies-bindings-wasm": "^0.1.7", + "@xmtp/proto": "^3.31.2", "async-mutex": "^0.4.0", "elliptic": "^6.5.4", "ethers": "^5.5.3", diff --git a/src/Contacts.ts b/src/Contacts.ts new file mode 100644 index 000000000..51873b26d --- /dev/null +++ b/src/Contacts.ts @@ -0,0 +1,199 @@ +import { Envelope } from '@xmtp/proto/ts/dist/types/message_api/v1/message_api.pb' +import Client from './Client' +import { + // eslint-disable-next-line camelcase + ecies_decrypt_k256_sha3_256, + // eslint-disable-next-line camelcase + ecies_encrypt_k256_sha3_256, + // eslint-disable-next-line camelcase + generate_private_preferences_topic, +} from '@xmtp/ecies-bindings-wasm' +import { privatePreferences } from '@xmtp/proto' +import { buildUserPrivatePreferencesTopic } from './utils' +import { PublishParams } from './ApiClient' + +export type AllowListPermissionType = 'allow' | 'block' | 'unknown' + +export type AllowListEntryType = 'address' + +export class AllowListEntry { + value: string + entryType: AllowListEntryType + permissionType: AllowListPermissionType + + constructor( + value: string, + entryType: AllowListEntryType, + permissionType: AllowListPermissionType + ) { + this.value = value + this.entryType = entryType + this.permissionType = permissionType + } + + get key(): string { + return `${this.entryType}-${this.value}` + } + + static fromAddress( + address: string, + permissionType: AllowListPermissionType = 'unknown' + ): AllowListEntry { + return new AllowListEntry(address, 'address', permissionType) + } +} + +export class AllowList { + entries: Map + + constructor() { + this.entries = new Map() + } + + allow(address: string) { + const entry = AllowListEntry.fromAddress(address, 'allow') + this.entries.set(address, 'allow') + return entry + } + + block(address: string) { + const entry = AllowListEntry.fromAddress(address, 'block') + this.entries.set(address, 'block') + return entry + } + + state(address: string) { + return this.entries.get(address) ?? 'unknown' + } + + static async load(client: Client): Promise { + const allowList = new AllowList() + const privateKeyBundle = await client.keystore.getPrivateKeyBundle() + const publicKey = + privateKeyBundle.identityKey?.publicKey?.secp256k1Uncompressed?.bytes + const privateKey = privateKeyBundle.identityKey?.secp256k1?.bytes + if (publicKey && privateKey) { + const identifier = + generate_private_preferences_topic(privateKey).toString() + const envelopes = ( + await client.listEnvelopes( + buildUserPrivatePreferencesTopic(identifier), + async ({ message }: Envelope) => { + if (message) { + const payload = ecies_decrypt_k256_sha3_256( + publicKey, + privateKey, + message + ) + return privatePreferences.PrivatePreferencesAction.decode(payload) + } + return undefined + } + ) + ).filter( + (envelope) => envelope !== undefined + ) as privatePreferences.PrivatePreferencesAction[] + + envelopes.forEach((envelope) => { + envelope.allow?.walletAddresses.forEach((address) => { + allowList.allow(address) + }) + envelope.block?.walletAddresses.forEach((address) => { + allowList.block(address) + }) + }) + } + + return allowList + } + + static async publish(entries: AllowListEntry[], client: Client) { + const privateKeyBundle = await client.keystore.getPrivateKeyBundle() + const publicKey = + privateKeyBundle.identityKey?.publicKey?.secp256k1Uncompressed?.bytes + const privateKey = privateKeyBundle.identityKey?.secp256k1?.bytes + + if (publicKey && privateKey) { + const identifier = + generate_private_preferences_topic(privateKey).toString() + + const envelopes = entries + .map((entry) => { + if (entry.entryType === 'address') { + const action: privatePreferences.PrivatePreferencesAction = { + allow: + entry.permissionType === 'allow' + ? { + walletAddresses: [entry.value], + } + : undefined, + block: + entry.permissionType === 'block' + ? { + walletAddresses: [entry.value], + } + : undefined, + } + const payload = + privatePreferences.PrivatePreferencesAction.encode( + action + ).finish() + const message = ecies_encrypt_k256_sha3_256( + publicKey, + privateKey, + payload + ) + return { + contentTopic: buildUserPrivatePreferencesTopic(identifier), + message, + timestamp: new Date(), + } + } + return undefined + }) + .filter((envelope) => envelope !== undefined) as PublishParams[] + await client.publishEnvelopes(envelopes) + } + } +} + +export class Contacts { + /** + * Addresses that the client has connected to + */ + addresses: Set + allowList: AllowList + client: Client + + constructor(client: Client) { + this.addresses = new Set() + this.allowList = new AllowList() + this.client = client + } + + async refreshAllowList() { + this.allowList = await AllowList.load(this.client) + } + + isAllowed(address: string) { + return this.allowList.state(address) === 'allow' + } + + isBlocked(address: string) { + return this.allowList.state(address) === 'block' + } + + async allow(addresses: string[]) { + await AllowList.publish( + addresses.map((address) => this.allowList.allow(address)), + this.client + ) + } + + async block(addresses: string[]) { + await AllowList.publish( + addresses.map((address) => this.allowList.block(address)), + this.client + ) + } +} diff --git a/src/utils/topic.ts b/src/utils/topic.ts index 5290cbf51..ce1003806 100644 --- a/src/utils/topic.ts +++ b/src/utils/topic.ts @@ -31,7 +31,11 @@ export const buildUserInviteTopic = (walletAddr: string): string => { // EIP55 normalize the address case. return buildContentTopic(`invite-${utils.getAddress(walletAddr)}`) } + export const buildUserPrivateStoreTopic = (addrPrefixedKey: string): string => { // e.g. "0x1111111111222222222233333333334444444444/key_bundle" return buildContentTopic(`privatestore-${addrPrefixedKey}`) } + +export const buildUserPrivatePreferencesTopic = (identifier: string) => + buildUserPrivateStoreTopic(`${identifier}/allowlist`)