Skip to content

Commit

Permalink
feat: add PPPP support
Browse files Browse the repository at this point in the history
  • Loading branch information
rygine committed Oct 25, 2023
1 parent 0980c94 commit 077725d
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 12 deletions.
14 changes: 10 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 10 additions & 3 deletions src/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { hasMetamaskWithSnaps } from './keystore/snapHelpers'
import { version as snapVersion, package as snapPackage } from './snapInfo.json'
import { ExtractDecodedType } from './types/client'
import type { WalletClient } from 'viem'
import { Contacts } from './Contacts'
const { Compression } = proto

// eslint-disable @typescript-eslint/explicit-module-boundary-types
Expand Down Expand Up @@ -194,6 +195,10 @@ export type PreEventCallbackOptions = {
preEnableIdentityCallback?: PreEventCallback
}

export type AllowListOptions = {
enableAllowList: boolean
}

/**
* Aggregate type for client options. Optional properties are used when the default value is calculated on invocation, and are computed
* as needed by each function. All other defaults are specified in defaultOptions.
Expand All @@ -203,7 +208,8 @@ export type ClientOptions = Flatten<
KeyStoreOptions &
ContentOptions &
LegacyOptions &
PreEventCallbackOptions
PreEventCallbackOptions &
AllowListOptions
>

/**
Expand All @@ -226,6 +232,7 @@ export function defaultOptions(opts?: Partial<ClientOptions>): ClientOptions {
disablePersistenceEncryption: false,
keystoreProviders: defaultKeystoreProviders(),
apiClientFactory: createHttpApiClientFromOptions,
enableAllowList: false,
}

if (opts?.codecs) {
Expand All @@ -251,7 +258,7 @@ export default class Client<ContentTypes = any> {
address: string
keystore: Keystore
apiClient: ApiClient
contacts: Set<string> // address which we have connected to
contacts: Contacts
publicKeyBundle: PublicKeyBundle
private knownPublicKeyBundles: Map<
string,
Expand All @@ -270,7 +277,7 @@ export default class Client<ContentTypes = any> {
backupClient: BackupClient,
keystore: Keystore
) {
this.contacts = new Set<string>()
this.contacts = new Contacts(this)
this.knownPublicKeyBundles = new Map<
string,
PublicKeyBundle | SignedPublicKeyBundle
Expand Down
199 changes: 199 additions & 0 deletions src/Contacts.ts
Original file line number Diff line number Diff line change
@@ -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<string, AllowListPermissionType>

constructor() {
this.entries = new Map<string, AllowListPermissionType>()
}

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<AllowList> {
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<string>
allowList: AllowList
client: Client

constructor(client: Client) {
this.addresses = new Set<string>()
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
)
}
}
24 changes: 20 additions & 4 deletions src/conversations/Conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,13 +237,13 @@ export class ConversationV1<ContentTypes>

const topic = options?.ephemeral ? this.ephemeralTopic : this.topic

if (!this.client.contacts.has(this.peerAddress)) {
if (!this.client.contacts.addresses.has(this.peerAddress)) {
topics = [
buildUserIntroTopic(this.peerAddress),
buildUserIntroTopic(this.client.address),
topic,
]
this.client.contacts.add(this.peerAddress)
this.client.contacts.addresses.add(this.peerAddress)
} else {
topics = [topic]
}
Expand Down Expand Up @@ -345,13 +345,13 @@ export class ConversationV1<ContentTypes>

const topic = options?.ephemeral ? this.ephemeralTopic : this.topic

if (!this.client.contacts.has(this.peerAddress)) {
if (!this.client.contacts.addresses.has(this.peerAddress)) {
topics = [
buildUserIntroTopic(this.peerAddress),
buildUserIntroTopic(this.client.address),
topic,
]
this.client.contacts.add(this.peerAddress)
this.client.contacts.addresses.add(this.peerAddress)
} else {
topics = [topic]
}
Expand Down Expand Up @@ -475,6 +475,22 @@ export class ConversationV2<ContentTypes>
return this.client.address
}

async allow() {
await this.client.contacts.allow([this.peerAddress])
}

async block() {
await this.client.contacts.block([this.peerAddress])
}

isAllowed() {
return this.client.contacts.isAllowed(this.peerAddress)
}

isBlocked() {
return this.client.contacts.isBlocked(this.peerAddress)
}

/**
* Returns a list of all messages to/from the peerAddress
*/
Expand Down
4 changes: 4 additions & 0 deletions src/utils/topic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`)

0 comments on commit 077725d

Please sign in to comment.