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 094e4d3
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 5 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
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
)
}
}
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 094e4d3

Please sign in to comment.