Fix key and topic generation, and remove dependencies (#285)

* update yarn.lock

* remove modules

* move client

* move account

* revert chat_identity.ts

* fix key gen

* fix topic gen

* fix non-null assertion

* fix build errors
This commit is contained in:
Felicio Mununga 2022-06-24 17:39:09 +02:00 committed by GitHub
parent 484c76ccbb
commit 6d30b8ac25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 58 additions and 2768 deletions

View File

@ -1,6 +0,0 @@
{
"extension": ["ts"],
"spec": "src/**/*.spec.ts",
"require": "ts-node/register",
"exit": true
}

View File

@ -1 +0,0 @@
# `status-js`

View File

@ -38,30 +38,14 @@
"proto:build": "buf generate" "proto:build": "buf generate"
}, },
"dependencies": { "dependencies": {
"bn.js": "^5.2.0",
"buffer": "^6.0.3",
"debug": "^4.3.3",
"ecies-geth": "^1.5.3",
"elliptic": "^6.5.4",
"ethereum-cryptography": "^1.0.3", "ethereum-cryptography": "^1.0.3",
"js-sha3": "^0.8.0",
"js-waku": "^0.23.0", "js-waku": "^0.23.0",
"long": "^5.2.0", "long": "^5.2.0",
"pbkdf2": "^3.1.2",
"protobufjs": "^6.11.3", "protobufjs": "^6.11.3",
"protons-runtime": "^1.0.4", "protons-runtime": "^1.0.4"
"secp256k1": "^4.0.2",
"uuid": "^8.3.2"
}, },
"devDependencies": { "devDependencies": {
"@types/bn.js": "^5.1.0",
"@types/elliptic": "^6.4.14",
"@types/pbkdf2": "^3.1.0",
"@types/secp256k1": "^4.0.3",
"@types/uuid": "^8.3.3",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"protons": "^3.0.4", "protons": "^3.0.4"
"ts-node": "^10.2.1",
"ts-proto": "^1.115.1"
} }
} }

View File

@ -1,87 +0,0 @@
import { idToContentTopic } from './contentTopic'
import { createSymKeyFromPassword } from './encryption'
import { ChatMessage } from './wire/chat_message'
import type { Content } from './wire/chat_message'
import type { CommunityChat } from './wire/community_chat'
/**
* Represent a chat room. Only public chats are currently supported.
*/
export class Chat {
private lastClockValue?: number
private lastMessage?: ChatMessage
private constructor(
public id: string,
public symKey: Uint8Array,
public communityChat?: CommunityChat
) {}
/**
* Create a public chat room.
* [[Community.instantiateChat]] MUST be used for chats belonging to a community.
*/
public static async create(
id: string,
communityChat?: CommunityChat
): Promise<Chat> {
const symKey = await createSymKeyFromPassword(id)
return new Chat(id, symKey, communityChat)
}
public get contentTopic(): string {
return idToContentTopic(this.id)
}
public createMessage(content: Content, responseTo?: string): ChatMessage {
const { timestamp, clock } = this._nextClockAndTimestamp()
const message = ChatMessage.createMessage(
clock,
timestamp,
this.id,
content,
responseTo
)
this._updateClockFromMessage(message)
return message
}
public handleNewMessage(message: ChatMessage): void {
this._updateClockFromMessage(message)
}
private _nextClockAndTimestamp(): { clock: number; timestamp: number } {
let clock = this.lastClockValue
const timestamp = Date.now()
if (!clock || clock < timestamp) {
clock = timestamp
} else {
clock += 1
}
return { clock, timestamp }
}
private _updateClockFromMessage(message: ChatMessage): void {
if (
!this.lastMessage ||
!this.lastMessage.clock ||
(message.clock && this.lastMessage.clock <= message.clock)
) {
this.lastMessage = message
}
if (
!this.lastClockValue ||
(message.clock && this.lastClockValue < message.clock)
) {
this.lastClockValue = message.clock
}
}
}

View File

@ -1,147 +0,0 @@
import { getPredefinedBootstrapNodes, Waku } from 'js-waku'
import { ApplicationMetadataMessage } from '../protos/application-metadata-message'
import { ChatMessage } from '../protos/chat-message'
import { CommunityChat, CommunityDescription } from '../protos/communities'
import { Account } from './account'
import { idToContentTopic } from './contentTopic'
import { createSymKeyFromPassword } from './encryption'
export interface ClientOptions {
publicKey: string
env?: 'production' | 'test'
callback: (message: ChatMessage) => void
}
export class Client {
options: ClientOptions
publicKey: string
callback: (message: ChatMessage) => void
waku?: Waku
account?: Account
communityDescription?: CommunityDescription
clocks: Record<string, Date>
constructor(options: ClientOptions) {
this.options = options
this.publicKey = options.publicKey
this.callback = options.callback
this.clocks = {}
}
public async start() {
console.log(getPredefinedBootstrapNodes('test'))
this.waku = await Waku.create(
this.options.env === 'test'
? {
bootstrap: {
peers: [
'/dns4/node-01.gc-us-central1-a.wakuv2.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAmJb2e28qLXxT5kZxVUUoJt72EMzNGXB47Rxx5hw3q4YjS',
],
},
}
: { bootstrap: { default: true } }
)
console.log('here')
await this.waku.waitForRemotePeer()
}
public async getCommunityDescription(): Promise<CommunityDescription> {
if (!this.waku) {
throw new Error('Waku not started')
}
const contentTopic = idToContentTopic(this.options.publicKey)
try {
// const symKey = await createSymKeyFromPassword(hexCommunityPublicKey)
const symKey = await createSymKeyFromPassword(this.options.publicKey)
await this.waku.store.queryHistory([contentTopic], {
callback: messages => {
for (const message of messages.reverse()) {
if (!message.payload) {
return
}
// try {
const metadata = ApplicationMetadataMessage.decode(message.payload)
if (!metadata.payload) {
return
}
const communityDescription = CommunityDescription.decode(
metadata.payload
)
if (communityDescription.identity) {
this.communityDescription = communityDescription
this.observeCommunityChats(communityDescription.chats)
return true
}
}
},
decryptionKeys: [symKey],
})
} catch (error) {
console.log(error)
throw error
}
if (!this.communityDescription) {
throw new Error('Community not found')
}
return this.communityDescription
}
private observeCommunityChats(chats: CommunityDescription['chats']) {
const contentTopics = Object.entries(chats).map(([chatUuid, chat]) => {
const chatId = `${this.publicKey}${chatUuid}`
return idToContentTopic(chatId)
})
this.waku!.relay.addObserver(this.handleMessage, contentTopics)
}
private async handleMessage(message: WakuMessage) {
if (!message.payload || !message.timestamp) {
return
}
// handle increment of Lamport clock
const { timestamp, payload } = message
const metadata = ApplicationMetadataMessage.decode(payload)
// decode and validate before sending to consumers of status-js
switch (metadata.type) {
case ApplicationMetadataMessage.Type.TYPE_CHAT_MESSAGE: {
const chatMessage = ChatMessage.decode(metadata.payload)
this.clocks[chatMessage.chatId] = timestamp
this.callback(chatMessage)
return
}
// case ApplicationMetadataMessage.Type.TYPE_EMOJI_REACTION: {
// return
// }
default: {
console.log('Unknown message type:', metadata.type)
}
}
}
createAccount = async (): Promise<Account> => {
this.account = new Account()
return this.account
}
}
export const createClient = async (options: ClientOptions) => {
const client = new Client(options)
await client.start()
return client
}

View File

@ -2,8 +2,8 @@ import { keccak256 } from 'ethereum-cryptography/keccak'
import { getPublicKey, sign, utils } from 'ethereum-cryptography/secp256k1' import { getPublicKey, sign, utils } from 'ethereum-cryptography/secp256k1'
import { bytesToHex, concatBytes } from 'ethereum-cryptography/utils' import { bytesToHex, concatBytes } from 'ethereum-cryptography/utils'
import { compressPublicKey } from './utils/compress-public-key' import { compressPublicKey } from '../utils/compress-public-key'
import { generateUsername } from './utils/generate-username' import { generateUsername } from '../utils/generate-username'
export class Account { export class Account {
public privateKey: string public privateKey: string

View File

@ -1,22 +1,24 @@
import { PageDirection } from 'js-waku' import { PageDirection } from 'js-waku'
import { import {
AudioMessage,
ChatMessage as ChatMessageProto, ChatMessage as ChatMessageProto,
DeleteMessage, DeleteMessage,
EditMessage, EditMessage,
ImageType,
} from '~/protos/chat-message' } from '~/protos/chat-message'
import { EmojiReaction } from '~/protos/emoji-reaction' import { EmojiReaction } from '~/protos/emoji-reaction'
import { idToContentTopic } from '../contentTopic'
import { createSymKeyFromPassword } from '../encryption'
import { containsOnlyEmoji } from '../helpers/contains-only-emoji' import { containsOnlyEmoji } from '../helpers/contains-only-emoji'
import { generateKeyFromPassword } from '../utils/generate-key-from-password'
import { idToContentTopic } from '../utils/id-to-content-topic'
import { getReactions } from './community/get-reactions' import { getReactions } from './community/get-reactions'
import type { MessageType } from '../../protos/enums' import type { MessageType } from '../../protos/enums'
import type { Client } from '../client' import type { Client } from './client'
import type { Community } from './community/community' import type { Community } from './community/community'
import type { Reactions } from './community/get-reactions' import type { Reactions } from './community/get-reactions'
import type { ImageMessage } from '~/src/proto/communities/v1/chat_message' import type { ImageMessage } from '~/protos/chat-message'
import type { CommunityChat } from '~/src/proto/communities/v1/communities' import type { CommunityChat } from '~/src/proto/communities/v1/communities'
import type { WakuMessage } from 'js-waku' import type { WakuMessage } from 'js-waku'
@ -89,7 +91,7 @@ export class Chat {
) => { ) => {
const id = `${community.publicKey}${uuid}` const id = `${community.publicKey}${uuid}`
const contentTopic = idToContentTopic(id) const contentTopic = idToContentTopic(id)
const symmetricKey = await createSymKeyFromPassword(id) const symmetricKey = await generateKeyFromPassword(id)
return new Chat({ return new Chat({
client, client,
@ -441,11 +443,11 @@ export class Chat {
messageType: 'COMMUNITY_CHAT' as MessageType, messageType: 'COMMUNITY_CHAT' as MessageType,
sticker: { hash: '', pack: 0 }, sticker: { hash: '', pack: 0 },
image: { image: {
type: 'JPEG', type: ImageType.JPEG,
payload: new Uint8Array([]), payload: new Uint8Array([]),
}, },
audio: { audio: {
type: 'AAC', type: AudioMessage.AudioType.AAC,
payload: new Uint8Array([]), payload: new Uint8Array([]),
durationMs: BigInt(0), durationMs: BigInt(0),
}, },
@ -478,7 +480,7 @@ export class Chat {
payload: image.payload, payload: image.payload,
}, },
audio: { audio: {
type: 'AAC', type: AudioMessage.AudioType.AAC,
payload: new Uint8Array([]), payload: new Uint8Array([]),
durationMs: BigInt(0), durationMs: BigInt(0),
}, },

View File

@ -8,8 +8,8 @@ import { Waku, WakuMessage } from 'js-waku'
import { ApplicationMetadataMessage } from '~/protos/application-metadata-message' import { ApplicationMetadataMessage } from '~/protos/application-metadata-message'
import { Account } from './account' import { Account } from './account'
import { Community } from './client/community/community' import { Community } from './community/community'
import { handleWakuMessage } from './client/community/handle-waku-message' import { handleWakuMessage } from './community/handle-waku-message'
export interface ClientOptions { export interface ClientOptions {
publicKey: string publicKey: string

View File

@ -6,13 +6,13 @@ import { MessageType } from '~/protos/enums'
import { getDifferenceByKeys } from '~/src/helpers/get-difference-by-keys' import { getDifferenceByKeys } from '~/src/helpers/get-difference-by-keys'
import { getObjectsDifference } from '~/src/helpers/get-objects-difference' import { getObjectsDifference } from '~/src/helpers/get-objects-difference'
import { compressPublicKey } from '~/src/utils/compress-public-key' import { compressPublicKey } from '~/src/utils/compress-public-key'
import { generateKeyFromPassword } from '~/src/utils/generate-key-from-password'
import { idToContentTopic } from '~/src/utils/id-to-content-topic'
import { idToContentTopic } from '../../contentTopic'
import { createSymKeyFromPassword } from '../../encryption'
import { Chat } from '../chat' import { Chat } from '../chat'
import { Member } from '../member' import { Member } from '../member'
import type { Client } from '../../client' import type { Client } from '../client'
import type { import type {
CommunityChat, CommunityChat,
CommunityDescription, CommunityDescription,
@ -44,7 +44,7 @@ export class Community {
public async start() { public async start() {
this.contentTopic = idToContentTopic(this.publicKey) this.contentTopic = idToContentTopic(this.publicKey)
this.symmetricKey = await createSymKeyFromPassword(this.publicKey) this.symmetricKey = await generateKeyFromPassword(this.publicKey)
// Waku // Waku
this.client.waku.store.addDecryptionKey(this.symmetricKey, { this.client.waku.store.addDecryptionKey(this.symmetricKey, {
@ -147,14 +147,24 @@ export class Community {
private unobserveChatMessages = ( private unobserveChatMessages = (
chatDescription: CommunityDescription['chats'] chatDescription: CommunityDescription['chats']
) => { ) => {
const contentTopics = Object.keys(chatDescription).map(chatUuid => { const contentTopics: string[] = []
for (const chatUuid of Object.keys(chatDescription)) {
const chat = this.chats.get(chatUuid) const chat = this.chats.get(chatUuid)
const contentTopic = chat!.contentTopic
if (!chat) {
continue
}
const contentTopic = chat.contentTopic
this.chats.delete(chatUuid) this.chats.delete(chatUuid)
contentTopics.push(contentTopic)
}
return contentTopic if (!contentTopics.length) {
}) return
}
this.client.waku.relay.deleteObserver( this.client.waku.relay.deleteObserver(
this.client.handleWakuMessage, this.client.handleWakuMessage,

View File

@ -16,7 +16,7 @@ import { recoverPublicKey } from '../../utils/recover-public-key'
import { getChatUuid } from './get-chat-uuid' import { getChatUuid } from './get-chat-uuid'
import { mapChatMessage } from './map-chat-message' import { mapChatMessage } from './map-chat-message'
import type { Client } from '../../client' import type { Client } from '../client'
import type { Community } from './community' import type { Community } from './community'
import type { WakuMessage } from 'js-waku' import type { WakuMessage } from 'js-waku'

View File

@ -1,172 +0,0 @@
// see
import { PageDirection, WakuMessage } from 'js-waku'
import { idToContactCodeTopic } from './contentTopic'
import { StatusUpdate_StatusType } from './proto/communities/v1/status_update'
import { bufToHex, getLatestUserNickname } from './utils'
import { ChatIdentity } from './wire/chat_identity'
import { StatusUpdate } from './wire/status_update'
import type { Identity } from './identity'
import type { Waku } from 'js-waku'
const STATUS_BROADCAST_INTERVAL = 30000
const NICKNAME_BROADCAST_INTERVAL = 300000
export class Contacts {
waku: Waku
identity: Identity | undefined
nickname?: string
private callback: (publicKey: string, clock: number) => void
private callbackNickname: (publicKey: string, nickname: string) => void
private contacts: string[] = []
/**
* Contacts holds a list of user contacts and listens to their status broadcast
*
* When watched user broadcast callback is called.
*
* Class also broadcasts own status on contact-code topic
*
* @param identity identity of user that is used to broadcast status message
*
* @param waku waku class used to listen to broadcast and broadcast status
*
* @param callback callback function called when user status broadcast is received
*/
public constructor(
identity: Identity | undefined,
waku: Waku,
callback: (publicKey: string, clock: number) => void,
callbackNickname: (publicKey: string, nickname: string) => void,
nickname?: string
) {
this.waku = waku
this.identity = identity
this.nickname = nickname
this.callback = callback
this.callbackNickname = callbackNickname
this.startBroadcast()
if (identity) {
this.addContact(bufToHex(identity.publicKey))
}
}
/**
* Add contact to watch list of status broadcast
*
* When user broadcasts its status callback is called
*
* @param publicKey public key of user
*/
public addContact(publicKey: string): void {
if (!this.contacts.find(e => publicKey === e)) {
const now = new Date()
const callback = (wakuMessage: WakuMessage): void => {
if (wakuMessage.payload) {
const msg = StatusUpdate.decode(wakuMessage.payload)
this.callback(publicKey, msg.clock ?? 0)
}
}
this.contacts.push(publicKey)
this.callback(publicKey, 0)
this.waku.store.queryHistory([idToContactCodeTopic(publicKey)], {
callback: msgs => msgs.forEach(e => callback(e)),
timeFilter: {
startTime: new Date(now.getTime() - STATUS_BROADCAST_INTERVAL * 2),
endTime: now,
},
})
this.waku.store.queryHistory([idToContactCodeTopic(publicKey)], {
callback: msgs =>
msgs.some(e => {
try {
if (e.payload) {
const chatIdentity = ChatIdentity.decode(e?.payload)
if (chatIdentity) {
this.callbackNickname(
publicKey,
chatIdentity?.displayName ?? ''
)
}
return true
}
} catch {
return false
}
}),
pageDirection: PageDirection.BACKWARD,
})
this.waku.relay.addObserver(callback, [idToContactCodeTopic(publicKey)])
}
}
private startBroadcast(): void {
const send = async (): Promise<void> => {
if (this.identity) {
const statusUpdate = StatusUpdate.create(
StatusUpdate_StatusType.AUTOMATIC,
''
)
const msg = await WakuMessage.fromBytes(
statusUpdate.encode(),
idToContactCodeTopic(bufToHex(this.identity.publicKey))
)
this.waku.relay.send(msg)
}
}
const handleNickname = async (): Promise<void> => {
if (this.identity) {
const now = new Date().getTime()
const { clock, nickname: newNickname } = await getLatestUserNickname(
this.identity.publicKey,
this.waku
)
if (this.nickname) {
if (this.nickname !== newNickname) {
await sendNickname()
} else {
if (clock < now - NICKNAME_BROADCAST_INTERVAL) {
await sendNickname()
}
}
} else {
this.nickname = newNickname
this.callbackNickname(bufToHex(this.identity.publicKey), newNickname)
if (clock < now - NICKNAME_BROADCAST_INTERVAL) {
await sendNickname()
}
}
}
setInterval(send, NICKNAME_BROADCAST_INTERVAL)
}
const sendNickname = async (): Promise<void> => {
if (this.identity) {
const publicKey = bufToHex(this.identity.publicKey)
if (this.nickname) {
const chatIdentity = new ChatIdentity({
clock: new Date().getTime(),
color: '',
description: '',
emoji: '',
images: {},
ensName: '',
displayName: this?.nickname ?? '',
})
const msg = await WakuMessage.fromBytes(
chatIdentity.encode(),
idToContactCodeTopic(publicKey),
{ sigPrivKey: this.identity.privateKey }
)
await this.waku.relay.send(msg)
}
}
}
handleNickname()
send()
setInterval(send, STATUS_BROADCAST_INTERVAL)
}
}

View File

@ -1,21 +0,0 @@
import { Buffer } from 'buffer'
import { keccak256 } from 'js-sha3'
const TopicLength = 4
/**
* Get the content topic of for a given Chat or Community
* @param id The Chat id or Community id (hex string prefixed with 0x).
* @returns string The Waku v2 Content Topic.
*/
export function idToContentTopic(id: string): string {
const hash = keccak256.arrayBuffer(id)
const topic = Buffer.from(hash).slice(0, TopicLength)
return '/waku/1/' + '0x' + topic.toString('hex') + '/rfc26'
}
export function idToContactCodeTopic(id: string): string {
return idToContentTopic(id + '-contact-code')
}

View File

@ -1,22 +0,0 @@
import { createSymKeyFromPassword } from './encryption'
describe('Encryption', () => {
test('Generate symmetric key from password', async () => {
const str = 'arbitrary data here'
const symKey = await createSymKeyFromPassword(str)
expect(Buffer.from(symKey).toString('hex')).toEqual(
'c49ad65ebf2a7b7253bf400e3d27719362a91b2c9b9f54d50a69117021666c33'
)
})
test('Generate symmetric key from password for chat', async () => {
const str =
'0x02dcec6041fb999d65f1d33363e08c93d3c1f6f0fbbb26add383e2cf46c2b921f41dc14fd8-9a8b-4df5-a358-2c3067be5439'
const symKey = await createSymKeyFromPassword(str)
expect(Buffer.from(symKey).toString('hex')).toEqual(
'76ff5bf0a74a8e724367c7fc003f066d477641f468768a8da2817addf5c2ce76'
)
})
})

View File

@ -1,25 +0,0 @@
import { utf8ToBytes } from 'ethereum-cryptography/utils'
import { pbkdf2 } from 'pbkdf2'
const AESKeyLength = 32 // bytes
export async function createSymKeyFromPassword(
password: string
): Promise<Uint8Array> {
return new Promise((resolve, reject) => {
pbkdf2(
utf8ToBytes(password),
'',
65356,
AESKeyLength,
'sha256',
(err, buf) => {
if (err) {
reject(err)
} else {
resolve(buf)
}
}
)
})
}

View File

@ -1,455 +0,0 @@
import { waku_message, WakuMessage } from 'js-waku'
// FIXME?: import from 'js-waku' not /build
import { ChatMessage } from '.'
import { createSymKeyFromPassword } from './encryption'
import { MembershipUpdateEvent_EventType } from './proto/communities/v1/membership_update_message'
import { getNegotiatedTopic, getPartitionedTopic } from './topics'
import { bufToHex, compressPublicKey } from './utils'
import { MembershipUpdateMessage } from './wire/membership_update_message'
import type { Content } from '.'
import type { Identity } from './identity'
import type { MembershipSignedEvent } from './wire/membership_update_message'
import type { Waku } from 'js-waku'
type GroupMember = {
id: string
topic: string
symKey: Uint8Array
partitionedTopic: string
}
export type GroupChat = {
chatId: string
members: GroupMember[]
admins?: string[]
name?: string
removed: boolean
}
export type GroupChatsType = {
[id: string]: GroupChat
}
/* TODO: add chat messages encryption */
class GroupChatUsers {
private users: { [id: string]: GroupMember } = {}
private identity: Identity
public constructor(_identity: Identity) {
this.identity = _identity
}
public async getUser(id: string): Promise<GroupMember> {
if (this.users[id]) {
return this.users[id]
}
const topic = await getNegotiatedTopic(this.identity, id)
const symKey = await createSymKeyFromPassword(topic)
const partitionedTopic = getPartitionedTopic(id)
const groupUser: GroupMember = { topic, symKey, id, partitionedTopic }
this.users[id] = groupUser
return groupUser
}
}
export class GroupChats {
waku: Waku
identity: Identity
private callback: (chats: GroupChat) => void
private removeCallback: (chats: GroupChat) => void
private addMessage: (message: ChatMessage, sender: string) => void
private groupChatUsers
public chats: GroupChatsType = {}
/**
* GroupChats holds a list of private chats and listens to their status broadcast
*
* @param identity identity of user
*
* @param waku waku class used to listen to broadcast and broadcast status
*
* @param callback callback function called when new private group chat is ceated
*
* @param removeCallback callback function when private group chat is to be removed
*
* @param addMessage callback function when
*/
public constructor(
identity: Identity,
waku: Waku,
callback: (chat: GroupChat) => void,
removeCallback: (chat: GroupChat) => void,
addMessage: (message: ChatMessage, sender: string) => void
) {
this.waku = waku
this.identity = identity
this.groupChatUsers = new GroupChatUsers(identity)
this.callback = callback
this.removeCallback = removeCallback
this.addMessage = addMessage
this.listen()
}
/**
* Send chat message on given private chat
*
* @param chatId chat id of private group chat
*
* @param text text message to send
*/
public async sendMessage(
chatId: string,
content: Content,
responseTo?: string
): Promise<void> {
const now = Date.now()
const chat = this.chats[chatId]
if (chat) {
await Promise.all(
chat.members.map(async member => {
const chatMessage = ChatMessage.createMessage(
now,
now,
chatId,
content,
responseTo
)
const wakuMessage = await WakuMessage.fromBytes(
chatMessage.encode(),
member.topic,
{ sigPrivKey: this.identity.privateKey, symKey: member.symKey }
)
this.waku.relay.send(wakuMessage)
})
)
}
}
private async handleUpdateEvent(
chatId: string,
event: MembershipSignedEvent,
useCallback: boolean
): Promise<void> {
const signer = event.signer ? bufToHex(event.signer) : ''
const thisUser = bufToHex(this.identity.publicKey)
const chat: GroupChat | undefined = this.chats[chatId]
if (signer) {
switch (event.event.type) {
case MembershipUpdateEvent_EventType.CHAT_CREATED: {
const members: GroupMember[] = []
await Promise.all(
event.event.members.map(async member => {
members.push(await this.groupChatUsers.getUser(member))
})
)
await this.addChat(
{
chatId: chatId,
members,
admins: [signer],
removed: false,
},
useCallback
)
break
}
case MembershipUpdateEvent_EventType.MEMBER_REMOVED: {
if (chat) {
chat.members = chat.members.filter(
member => !event.event.members.includes(member.id)
)
if (event.event.members.includes(thisUser)) {
await this.removeChat(
{
...chat,
removed: true,
},
useCallback
)
} else {
if (!chat.removed && useCallback) {
this.callback(this.chats[chatId])
}
}
}
break
}
case MembershipUpdateEvent_EventType.MEMBERS_ADDED: {
if (chat && chat.admins?.includes(signer)) {
const members: GroupMember[] = []
await Promise.all(
event.event.members.map(async member => {
members.push(await this.groupChatUsers.getUser(member))
})
)
chat.members.push(...members)
if (chat.members.findIndex(member => member.id === thisUser) > -1) {
chat.removed = false
await this.addChat(chat, useCallback)
}
}
break
}
case MembershipUpdateEvent_EventType.NAME_CHANGED: {
if (chat) {
if (chat.admins?.includes(signer)) {
chat.name = event.event.name
this.callback(chat)
}
}
break
}
}
}
}
private async decodeUpdateMessage(
message: WakuMessage,
useCallback: boolean
): Promise<void> {
try {
if (message?.payload) {
const membershipUpdate = MembershipUpdateMessage.decode(message.payload)
await Promise.all(
membershipUpdate.events.map(
async event =>
await this.handleUpdateEvent(
membershipUpdate.chatId,
event,
useCallback
)
)
)
}
} catch {
return
}
}
private handleWakuChatMessage(
message: WakuMessage,
chat: GroupChat,
member: string
): void {
try {
if (message.payload) {
const chatMessage = ChatMessage.decode(message.payload)
if (chatMessage) {
if (chatMessage.chatId === chat.chatId) {
let sender = member
if (message.signaturePublicKey) {
sender = compressPublicKey(message.signaturePublicKey)
}
this.addMessage(chatMessage, sender)
}
}
}
} catch {
return
}
}
private async handleChatObserver(
chat: GroupChat,
removeObserver?: boolean
): Promise<void> {
const observerFunction = removeObserver ? 'deleteObserver' : 'addObserver'
await Promise.all(
chat.members.map(async member => {
if (!removeObserver) {
this.waku.relay.addDecryptionKey(member.symKey, {
method: waku_message.DecryptionMethod.Symmetric,
contentTopics: [member.topic],
})
}
this.waku.relay[observerFunction](
message => this.handleWakuChatMessage(message, chat, member.id),
[member.topic]
)
})
)
}
private async addChat(chat: GroupChat, useCallback: boolean): Promise<void> {
if (this.chats[chat.chatId]) {
this.chats[chat.chatId] = chat
if (useCallback) {
this.callback(chat)
}
} else {
this.chats[chat.chatId] = chat
if (useCallback) {
await this.handleChatObserver(chat)
this.callback(chat)
}
}
}
private async removeChat(
chat: GroupChat,
useCallback: boolean
): Promise<void> {
this.chats[chat.chatId] = chat
if (useCallback) {
await this.handleChatObserver(chat, true)
this.removeCallback(chat)
}
}
private async listen(): Promise<void> {
const topic = getPartitionedTopic(bufToHex(this.identity.publicKey))
const messages = await this.waku.store.queryHistory([topic])
messages.sort((a, b) =>
(a?.timestamp?.getTime() ?? 0) < (b?.timestamp?.getTime() ?? 0) ? -1 : 1
)
for (let i = 0; i < messages.length; i++) {
await this.decodeUpdateMessage(messages[i], false)
}
this.waku.relay.addObserver(
message => this.decodeUpdateMessage(message, true),
[topic]
)
await Promise.all(
Object.values(this.chats).map(async chat => {
if (!chat?.removed) {
await this.handleChatObserver(chat)
this.callback(chat)
}
})
)
}
private async sendUpdateMessage(
payload: Uint8Array,
members: GroupMember[]
): Promise<void> {
const wakuMessages = await Promise.all(
members.map(
async member =>
await WakuMessage.fromBytes(payload, member.partitionedTopic)
)
)
wakuMessages.forEach(msg => this.waku.relay.send(msg))
}
/**
* Sends a change chat name chat membership update message
*
* @param chatId a chat id to which message is to be sent
*
* @param name a name which chat should be changed to
*/
public async changeChatName(chatId: string, name: string): Promise<void> {
const payload = MembershipUpdateMessage.create(chatId, this.identity)
const chat = this.chats[chatId]
if (chat && payload) {
payload.addNameChangeEvent(name)
await this.sendUpdateMessage(payload.encode(), chat.members)
}
}
/**
* Sends a add members group chat membership update message with given members
*
* @param chatId a chat id to which message is to be sent
*
* @param members a list of members to be added
*/
public async addMembers(chatId: string, members: string[]): Promise<void> {
const payload = MembershipUpdateMessage.create(chatId, this.identity)
const chat = this.chats[chatId]
if (chat && payload) {
const newMembers: GroupMember[] = []
await Promise.all(
members
.filter(
member =>
!chat.members.map(chatMember => chatMember.id).includes(member)
)
.map(async member => {
newMembers.push(await this.groupChatUsers.getUser(member))
})
)
payload.addMembersAddedEvent(newMembers.map(member => member.id))
await this.sendUpdateMessage(payload.encode(), [
...chat.members,
...newMembers,
])
}
}
/**
* Sends a create group chat membership update message with given members
*
* @param members a list of public keys of members to be included in private group chat
*/
public async createGroupChat(members: string[]): Promise<void> {
const payload = MembershipUpdateMessage.createChat(
this.identity,
members
).encode()
const newMembers: GroupMember[] = []
await Promise.all(
members.map(async member => {
newMembers.push(await this.groupChatUsers.getUser(member))
})
)
await this.sendUpdateMessage(payload, newMembers)
}
/**
* Sends a remove member to private group chat
*
* @param chatId id of private group chat
*/
public async quitChat(chatId: string): Promise<void> {
const payload = MembershipUpdateMessage.create(chatId, this.identity)
const chat = this.chats[chatId]
payload.addMemberRemovedEvent(bufToHex(this.identity.publicKey))
await this.sendUpdateMessage(payload.encode(), chat.members)
}
/**
* Retrieve previous messages from a Waku Store node for the given chat Id.
*
*/
public async retrievePreviousMessages(
chatId: string,
startTime: Date,
endTime: Date
): Promise<number> {
const chat = this.chats[chatId]
if (!chat)
throw `Failed to retrieve messages, chat is not joined: ${chatId}`
const _callback = (wakuMessages: WakuMessage[], member: string): void => {
wakuMessages.forEach((wakuMessage: WakuMessage) =>
this.handleWakuChatMessage(wakuMessage, chat, member)
)
}
const amountOfMessages: number[] = []
await Promise.all(
chat.members.map(async member => {
const msgLength = (
await this.waku.store.queryHistory([member.topic], {
timeFilter: { startTime, endTime },
callback: msg => _callback(msg, member.id),
decryptionKeys: [member.symKey],
})
).length
amountOfMessages.push(msgLength)
})
)
return amountOfMessages.reduce((a, b) => a + b)
}
}

View File

@ -1,38 +0,0 @@
import { Buffer } from 'buffer'
import { keccak256 } from 'js-sha3'
import { generatePrivateKey } from 'js-waku'
import * as secp256k1 from 'secp256k1'
import { hexToBuf } from './utils'
export class Identity {
private pubKey: Uint8Array
public constructor(public privateKey: Uint8Array) {
this.pubKey = secp256k1.publicKeyCreate(this.privateKey, true)
}
public static generate(): Identity {
const privateKey = generatePrivateKey()
return new Identity(privateKey)
}
/**
* Hashes the payload with SHA3-256 and signs the result using the internal private key.
*/
public sign(payload: Uint8Array): Uint8Array {
const hash = keccak256(payload)
const { signature, recid } = secp256k1.ecdsaSign(
hexToBuf(hash),
this.privateKey
)
return Buffer.concat([signature, Buffer.from([recid])])
}
/**
* Returns the compressed public key.
*/
public get publicKey(): Uint8Array {
return this.pubKey
}
}

View File

@ -1,6 +1,6 @@
export type { Account } from './account' export type { Account } from './client/account'
export type { Client, ClientOptions } from './client'
export { createClient } from './client'
export type { ChatMessage as Message } from './client/chat' export type { ChatMessage as Message } from './client/chat'
export type { Client, ClientOptions } from './client/client'
export { createClient } from './client/client'
export type { Community } from './client/community/community' export type { Community } from './client/community/community'
export type { Member } from './client/member' export type { Member } from './client/member'

View File

@ -1,7 +0,0 @@
import { keccak256 } from 'ethereum-cryptography/keccak'
import { getPublicKey, sign, utils } from 'ethereum-cryptography/secp256k1'
import { bytesToHex } from 'ethereum-cryptography/utils'
export class Members {
constructor() {}
}

View File

@ -1,103 +0,0 @@
import debug from 'debug'
import { Protocols } from 'js-waku'
import { Identity } from './identity'
import { Messenger } from './messenger'
import { bufToHex } from './utils'
import { ContentType } from './wire/chat_message'
import type { ApplicationMetadataMessage } from './wire/application_metadata_message'
const testChatId = 'test-chat-id'
const dbg = debug('communities:test:messenger')
describe('Messenger', () => {
let messengerAlice: Messenger
let messengerBob: Messenger
let identityAlice: Identity
let identityBob: Identity
beforeEach(async () => {
dbg('Generate keys')
identityAlice = Identity.generate()
identityBob = Identity.generate()
dbg('Create messengers')
;[messengerAlice, messengerBob] = await Promise.all([
Messenger.create(identityAlice, { bootstrap: {} }),
Messenger.create(identityBob, {
bootstrap: {},
libp2p: { addresses: { listen: ['/ip4/0.0.0.0/tcp/0/ws'] } },
}),
])
dbg('Connect messengers')
// Connect both messengers together for test purposes
messengerAlice.waku.addPeerToAddressBook(
messengerBob.waku.libp2p.peerId,
messengerBob.waku.libp2p.multiaddrs
)
dbg('Wait for remote peer')
await Promise.all([
messengerAlice.waku.waitForRemotePeer([Protocols.Relay]),
messengerBob.waku.waitForRemotePeer([Protocols.Relay]),
])
dbg('Messengers ready')
})
test('Sends & Receive public chat messages', async () => {
await messengerAlice.joinChatById(testChatId)
await messengerBob.joinChatById(testChatId)
const text = 'This is a message.'
const receivedMessagePromise: Promise<ApplicationMetadataMessage> =
new Promise(resolve => {
messengerBob.addObserver(message => {
resolve(message)
}, testChatId)
})
await messengerAlice.sendMessage(testChatId, {
text,
contentType: ContentType.Text,
})
const receivedMessage = await receivedMessagePromise
expect(receivedMessage.chatMessage?.text).toEqual(text)
})
test('public chat messages have signers', async () => {
await messengerAlice.joinChatById(testChatId)
await messengerBob.joinChatById(testChatId)
const text = 'This is a message.'
const receivedMessagePromise: Promise<ApplicationMetadataMessage> =
new Promise(resolve => {
messengerBob.addObserver(message => {
resolve(message)
}, testChatId)
})
await messengerAlice.sendMessage(testChatId, {
text,
contentType: ContentType.Text,
})
const receivedMessage = await receivedMessagePromise
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expect(bufToHex(receivedMessage.signer!)).toEqual(
bufToHex(identityAlice.publicKey)
)
})
afterEach(async () => {
await messengerAlice.stop()
await messengerBob.stop()
})
})

View File

@ -1,269 +0,0 @@
import debug from 'debug'
import { Waku, waku_message, WakuMessage } from 'js-waku'
import { Chat } from './chat'
import { ApplicationMetadataMessage_Type } from './proto/status/v1/application_metadata_message'
import { getLatestUserNickname } from './utils'
import { ApplicationMetadataMessage } from './wire/application_metadata_message'
import { ChatMessage } from './wire/chat_message'
import type { Identity } from './identity'
import type { Content } from './wire/chat_message'
import type { waku } from 'js-waku'
const dbg = debug('communities:messenger')
export class Messenger {
waku: Waku
chatsById: Map<string, Chat>
observers: {
[chatId: string]: Set<
(
message: ApplicationMetadataMessage,
timestamp: Date,
chatId: string
) => void
>
}
identity: Identity | undefined
private constructor(identity: Identity | undefined, waku: Waku) {
this.identity = identity
this.waku = waku
this.chatsById = new Map()
this.observers = {}
}
public static async create(
identity: Identity | undefined,
wakuOptions?: waku.CreateOptions
): Promise<Messenger> {
const _wakuOptions = Object.assign(
{ bootstrap: { default: true } },
wakuOptions
)
const waku = await Waku.create(_wakuOptions)
return new Messenger(identity, waku)
}
/**
* Joins a public chat using its id.
*
* For community chats, prefer [[joinChat]].
*
* Use `addListener` to get messages received on this chat.
*/
public async joinChatById(chatId: string): Promise<void> {
const chat = await Chat.create(chatId)
await this.joinChat(chat)
}
/**
* Joins several of public chats.
*
* Use `addListener` to get messages received on these chats.
*/
public async joinChats(chats: Iterable<Chat>): Promise<void> {
await Promise.all(
Array.from(chats).map(chat => {
return this.joinChat(chat)
})
)
}
/**
* Joins a public chat.
*
* Use `addListener` to get messages received on this chat.
*/
public async joinChat(chat: Chat): Promise<void> {
if (this.chatsById.has(chat.id))
throw `Failed to join chat, it is already joined: ${chat.id}`
this.waku.addDecryptionKey(chat.symKey, {
method: waku_message.DecryptionMethod.Symmetric,
contentTopics: [chat.contentTopic],
})
this.waku.relay.addObserver(
(wakuMessage: WakuMessage) => {
if (!wakuMessage.payload || !wakuMessage.timestamp) return
const message = ApplicationMetadataMessage.decode(wakuMessage.payload)
switch (message.type) {
case ApplicationMetadataMessage_Type.TYPE_CHAT_MESSAGE:
this._handleNewChatMessage(chat, message, wakuMessage.timestamp)
break
default:
dbg('Received unsupported message type', message.type)
}
},
[chat.contentTopic]
)
this.chatsById.set(chat.id, chat)
}
/**
* Sends a message on the given chat Id.
*/
public async sendMessage(
chatId: string,
content: Content,
responseTo?: string
): Promise<void> {
if (this.identity) {
const chat = this.chatsById.get(chatId)
if (!chat) throw `Failed to send message, chat not joined: ${chatId}`
const chatMessage = chat.createMessage(content, responseTo)
const appMetadataMessage = ApplicationMetadataMessage.create(
chatMessage.encode(),
ApplicationMetadataMessage_Type.TYPE_CHAT_MESSAGE,
this.identity
)
const wakuMessage = await WakuMessage.fromBytes(
appMetadataMessage.encode(),
chat.contentTopic,
{ symKey: chat.symKey, sigPrivKey: this.identity.privateKey }
)
await this.waku.relay.send(wakuMessage)
}
}
/**
* Add an observer of new messages received on the given chat id.
*
* @throws string If the chat has not been joined first using [joinChat].
*/
public addObserver(
observer: (
message: ApplicationMetadataMessage,
timestamp: Date,
chatId: string
) => void,
chatId: string | string[]
): void {
let chats = []
if (typeof chatId === 'string') {
chats.push(chatId)
} else {
chats = [...chatId]
}
chats.forEach(id => {
if (!this.chatsById.has(id))
throw 'Cannot add observer on a chat that is not joined.'
if (!this.observers[id]) {
this.observers[id] = new Set()
}
this.observers[id].add(observer)
})
}
/**
* Delete an observer of new messages received on the given chat id.
*
* @throws string If the chat has not been joined first using [joinChat].
*/
deleteObserver(
observer: (message: ApplicationMetadataMessage) => void,
chatId: string
): void {
if (this.observers[chatId]) {
this.observers[chatId].delete(observer)
}
}
/**
* Stops the messenger.
*/
public async stop(): Promise<void> {
await this.waku.stop()
}
/**
* Retrieve previous messages from a Waku Store node for the given chat Id.
*
* Note: note sure what is the preferred interface: callback or returning all messages
* Callback is more flexible and allow processing messages as they are retrieved instead of waiting for the
* full retrieval via paging to be done.
*/
public async retrievePreviousMessages(
chatId: string,
startTime: Date,
endTime: Date,
callback?: (messages: ApplicationMetadataMessage[]) => void
): Promise<number> {
const chat = this.chatsById.get(chatId)
if (!chat)
throw `Failed to retrieve messages, chat is not joined: ${chatId}`
const _callback = (wakuMessages: WakuMessage[]): void => {
const isDefined = (
msg: ApplicationMetadataMessage | undefined
): msg is ApplicationMetadataMessage => {
return !!msg
}
const messages = wakuMessages.map((wakuMessage: WakuMessage) => {
if (!wakuMessage.payload || !wakuMessage.timestamp) return
const message = ApplicationMetadataMessage.decode(wakuMessage.payload)
switch (message.type) {
case ApplicationMetadataMessage_Type.TYPE_CHAT_MESSAGE:
this._handleNewChatMessage(chat, message, wakuMessage.timestamp)
return message
default:
dbg('Retrieved unsupported message type', message.type)
return
}
})
if (callback) {
callback(messages.filter(isDefined))
}
}
const allMessages = await this.waku.store.queryHistory(
[chat.contentTopic],
{
timeFilter: { startTime, endTime },
callback: _callback,
}
)
return allMessages.length
}
private _handleNewChatMessage(
chat: Chat,
message: ApplicationMetadataMessage,
timestamp: Date
): void {
if (!message.payload || !message.type || !message.signature) return
const chatMessage = ChatMessage.decode(message.payload)
chat.handleNewMessage(chatMessage)
if (this.observers[chat.id]) {
this.observers[chat.id].forEach(observer => {
observer(message, timestamp, chat.id)
})
}
}
async checkIfUserInWakuNetwork(publicKey: Uint8Array): Promise<boolean> {
const { clock, nickname } = await getLatestUserNickname(
publicKey,
this.waku
)
return clock > 0 && nickname !== ''
}
}

View File

@ -1,212 +0,0 @@
/* eslint-disable */
import Long from 'long'
import _m0 from 'protobufjs/minimal'
export const protobufPackage = 'communities.v1'
/**
* Specs:
* :AUTOMATIC
* To Send - "AUTOMATIC" status ping every 5 minutes
* Display - Online for up to 5 minutes from the last clock, after that Offline
* :ALWAYS_ONLINE
* To Send - "ALWAYS_ONLINE" status ping every 5 minutes
* Display - Online for up to 2 weeks from the last clock, after that Offline
* :INACTIVE
* To Send - A single "INACTIVE" status ping
* Display - Offline forever
* Note: Only send pings if the user interacted with the app in the last x minutes.
*/
export interface StatusUpdate {
clock: number
statusType: StatusUpdate_StatusType
customText: string
}
export enum StatusUpdate_StatusType {
UNKNOWN_STATUS_TYPE = 0,
AUTOMATIC = 1,
DO_NOT_DISTURB = 2,
ALWAYS_ONLINE = 3,
INACTIVE = 4,
UNRECOGNIZED = -1,
}
export function statusUpdate_StatusTypeFromJSON(
object: any
): StatusUpdate_StatusType {
switch (object) {
case 0:
case 'UNKNOWN_STATUS_TYPE':
return StatusUpdate_StatusType.UNKNOWN_STATUS_TYPE
case 1:
case 'AUTOMATIC':
return StatusUpdate_StatusType.AUTOMATIC
case 2:
case 'DO_NOT_DISTURB':
return StatusUpdate_StatusType.DO_NOT_DISTURB
case 3:
case 'ALWAYS_ONLINE':
return StatusUpdate_StatusType.ALWAYS_ONLINE
case 4:
case 'INACTIVE':
return StatusUpdate_StatusType.INACTIVE
case -1:
case 'UNRECOGNIZED':
default:
return StatusUpdate_StatusType.UNRECOGNIZED
}
}
export function statusUpdate_StatusTypeToJSON(
object: StatusUpdate_StatusType
): string {
switch (object) {
case StatusUpdate_StatusType.UNKNOWN_STATUS_TYPE:
return 'UNKNOWN_STATUS_TYPE'
case StatusUpdate_StatusType.AUTOMATIC:
return 'AUTOMATIC'
case StatusUpdate_StatusType.DO_NOT_DISTURB:
return 'DO_NOT_DISTURB'
case StatusUpdate_StatusType.ALWAYS_ONLINE:
return 'ALWAYS_ONLINE'
case StatusUpdate_StatusType.INACTIVE:
return 'INACTIVE'
default:
return 'UNKNOWN'
}
}
const baseStatusUpdate: object = { clock: 0, statusType: 0, customText: '' }
export const StatusUpdate = {
encode(
message: StatusUpdate,
writer: _m0.Writer = _m0.Writer.create()
): _m0.Writer {
if (message.clock !== 0) {
writer.uint32(8).uint64(message.clock)
}
if (message.statusType !== 0) {
writer.uint32(16).int32(message.statusType)
}
if (message.customText !== '') {
writer.uint32(26).string(message.customText)
}
return writer
},
decode(input: _m0.Reader | Uint8Array, length?: number): StatusUpdate {
const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = { ...baseStatusUpdate } as StatusUpdate
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1:
message.clock = longToNumber(reader.uint64() as Long)
break
case 2:
message.statusType = reader.int32() as any
break
case 3:
message.customText = reader.string()
break
default:
reader.skipType(tag & 7)
break
}
}
return message
},
fromJSON(object: any): StatusUpdate {
const message = { ...baseStatusUpdate } as StatusUpdate
if (object.clock !== undefined && object.clock !== null) {
message.clock = Number(object.clock)
} else {
message.clock = 0
}
if (object.statusType !== undefined && object.statusType !== null) {
message.statusType = statusUpdate_StatusTypeFromJSON(object.statusType)
} else {
message.statusType = 0
}
if (object.customText !== undefined && object.customText !== null) {
message.customText = String(object.customText)
} else {
message.customText = ''
}
return message
},
toJSON(message: StatusUpdate): unknown {
const obj: any = {}
message.clock !== undefined && (obj.clock = message.clock)
message.statusType !== undefined &&
(obj.statusType = statusUpdate_StatusTypeToJSON(message.statusType))
message.customText !== undefined && (obj.customText = message.customText)
return obj
},
fromPartial(object: DeepPartial<StatusUpdate>): StatusUpdate {
const message = { ...baseStatusUpdate } as StatusUpdate
if (object.clock !== undefined && object.clock !== null) {
message.clock = object.clock
} else {
message.clock = 0
}
if (object.statusType !== undefined && object.statusType !== null) {
message.statusType = object.statusType
} else {
message.statusType = 0
}
if (object.customText !== undefined && object.customText !== null) {
message.customText = object.customText
} else {
message.customText = ''
}
return message
},
}
declare var self: any | undefined
declare var window: any | undefined
declare var global: any | undefined
var globalThis: any = (() => {
if (typeof globalThis !== 'undefined') return globalThis
if (typeof self !== 'undefined') return self
if (typeof window !== 'undefined') return window
if (typeof global !== 'undefined') return global
throw 'Unable to locate global object'
})()
type Builtin =
| Date
| Function
| Uint8Array
| string
| number
| boolean
| undefined
export type DeepPartial<T> = T extends Builtin
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>
function longToNumber(long: Long): number {
if (long.gt(Number.MAX_SAFE_INTEGER)) {
throw new globalThis.Error('Value is larger than Number.MAX_SAFE_INTEGER')
}
return long.toNumber()
}
if (_m0.util.Long !== Long) {
_m0.util.Long = Long as any
_m0.configure()
}

View File

@ -1,45 +0,0 @@
import { BN } from 'bn.js'
import { derive } from 'ecies-geth'
import { ec } from 'elliptic'
import { idToContentTopic } from './contentTopic'
import { bufToHex, hexToBuf } from './utils'
import type { Identity } from './identity'
const EC = new ec('secp256k1')
const partitionsNum = new BN(5000)
/**
* Get the partitioned topic https://specs.status.im/spec/3#partitioned-topic
* @param publicKey Public key of recipient
* @returns string The Waku v2 Content Topic.
*/
export function getPartitionedTopic(publicKey: string): string {
const key = EC.keyFromPublic(publicKey.slice(2), 'hex')
const X = key.getPublic().getX()
const partition = X.mod(partitionsNum)
const partitionTopic = `contact-discovery-${partition.toString()}`
return idToContentTopic(partitionTopic)
}
/**
* Get the negotiated topic https://specs.status.im/spec/3#negotiated-topic
* @param identity identity of user
* @param publicKey Public key of recipient
* @returns string The Waku v2 Content Topic.
*/
export async function getNegotiatedTopic(
identity: Identity,
publicKey: string
): Promise<string> {
const key = EC.keyFromPublic(publicKey.slice(2), 'hex')
const sharedSecret = await derive(
Buffer.from(identity.privateKey),
Buffer.concat([hexToBuf(key.getPublic('hex'))])
)
return idToContentTopic(bufToHex(sharedSecret))
}

View File

@ -1,61 +0,0 @@
import { ec } from 'elliptic'
import { PageDirection, utils } from 'js-waku'
import { idToContactCodeTopic } from './contentTopic'
import { ChatIdentity } from './proto/communities/v1/chat_identity'
import type { Waku } from 'js-waku'
const EC = new ec('secp256k1')
// TODO: rename
const hexToBuf = utils.hexToBytes
export { hexToBuf }
// TODO: rename
/**
* Return hex string with 0x prefix (commonly used for string format of a community id/public key.
*/
export function bufToHex(buf: Uint8Array): string {
return '0x' + utils.bytesToHex(buf)
}
export function compressPublicKey(key: Uint8Array): string {
const PubKey = EC.keyFromPublic(key)
return '0x' + PubKey.getPublic(true, 'hex')
}
export function genPrivateKeyWithEntropy(key: string): Uint8Array {
const pair = EC.genKeyPair({ entropy: key })
return hexToBuf('0x' + pair.getPrivate('hex'))
}
export async function getLatestUserNickname(
key: Uint8Array,
waku: Waku
): Promise<{ clock: number; nickname: string }> {
const publicKey = bufToHex(key)
let nickname = ''
let clock = 0
await waku.store.queryHistory([idToContactCodeTopic(publicKey)], {
callback: msgs =>
msgs.some(e => {
try {
if (e.payload) {
const chatIdentity = ChatIdentity.decode(e?.payload)
if (chatIdentity) {
if (chatIdentity?.displayName) {
clock = chatIdentity?.clock ?? 0
nickname = chatIdentity?.displayName
}
}
return true
}
} catch {
return false
}
}),
pageDirection: PageDirection.BACKWARD,
})
return { clock, nickname }
}

View File

@ -4,7 +4,7 @@ import { generateKeyFromPassword } from './generate-key-from-password'
describe('createSymKeyFromPassword', () => { describe('createSymKeyFromPassword', () => {
it('should create symmetric key from password', async () => { it('should create symmetric key from password', async () => {
const password = 'password' const password = 'arbitrary data here'
const symKey = await generateKeyFromPassword(password) const symKey = await generateKeyFromPassword(password)
expect(bytesToHex(symKey)).toEqual( expect(bytesToHex(symKey)).toEqual(

View File

@ -1,5 +1,5 @@
import { pbkdf2 } from 'ethereum-cryptography/pbkdf2' import { pbkdf2 } from 'ethereum-cryptography/pbkdf2'
import { hexToBytes, utf8ToBytes } from 'ethereum-cryptography/utils' import { utf8ToBytes } from 'ethereum-cryptography/utils'
const AES_KEY_LENGTH = 32 // bytes const AES_KEY_LENGTH = 32 // bytes
@ -10,7 +10,7 @@ export async function generateKeyFromPassword(
password: string password: string
): Promise<Uint8Array> { ): Promise<Uint8Array> {
return await pbkdf2( return await pbkdf2(
hexToBytes(password), utf8ToBytes(password),
utf8ToBytes(''), utf8ToBytes(''),
65356, 65356,
AES_KEY_LENGTH, AES_KEY_LENGTH,

View File

@ -1,5 +1,5 @@
import { keccak256 } from 'ethereum-cryptography/keccak' import { keccak256 } from 'ethereum-cryptography/keccak'
import { bytesToHex, hexToBytes } from 'ethereum-cryptography/utils' import { bytesToHex, utf8ToBytes } from 'ethereum-cryptography/utils'
/** /**
* waku spec: https://rfc.vac.dev/spec/23/#bridging-waku-v1-and-waku-v2 * waku spec: https://rfc.vac.dev/spec/23/#bridging-waku-v1-and-waku-v2
@ -9,8 +9,8 @@ import { bytesToHex, hexToBytes } from 'ethereum-cryptography/utils'
const TOPIC_LENGTH = 4 const TOPIC_LENGTH = 4
export function idToContentTopic(id: string): string { export function idToContentTopic(id: string): string {
const hash = keccak256(hexToBytes(id)) const hash = keccak256(utf8ToBytes(id))
const topic = hash.slice(0, TOPIC_LENGTH) const topic = hash.slice(0, TOPIC_LENGTH)
return `/waku/1/${bytesToHex(topic)}/rfc26` return `/waku/1/0x${bytesToHex(topic)}/rfc26`
} }

View File

@ -1,9 +1,9 @@
import { bytesToHex, utf8ToBytes } from 'ethereum-cryptography/utils' import { bytesToHex, utf8ToBytes } from 'ethereum-cryptography/utils'
import { Account } from '../account' import { Account } from '../client/account'
import { recoverPublicKey } from './recover-public-key' import { recoverPublicKey } from './recover-public-key'
import type { ApplicationMetadataMessage } from '~/protos/application-metadata-message' import type { ApplicationMetadataMessage } from '../../protos/application-metadata-message'
describe('recoverPublicKey', () => { describe('recoverPublicKey', () => {
it('should recover public key', async () => { it('should recover public key', async () => {

View File

@ -1,75 +0,0 @@
import { keccak256 } from 'js-sha3'
import { Reader } from 'protobufjs'
import secp256k1 from 'secp256k1'
import * as proto from '../proto/status/v1/application_metadata_message'
import { hexToBuf } from '../utils'
import { ChatMessage } from './chat_message'
import type { Identity } from '../identity'
import type { ApplicationMetadataMessage_Type } from '../proto/status/v1/application_metadata_message'
export class ApplicationMetadataMessage {
private constructor(public proto: proto.ApplicationMetadataMessage) {}
/**
* Create a chat message to be sent to an Open (permission = no membership) community
*/
public static create(
payload: Uint8Array,
type: ApplicationMetadataMessage_Type,
identity: Identity
): ApplicationMetadataMessage {
const signature = identity.sign(payload)
const proto = {
signature,
payload,
type,
}
return new ApplicationMetadataMessage(proto)
}
static decode(bytes: Uint8Array): ApplicationMetadataMessage {
const protoBuf = proto.ApplicationMetadataMessage.decode(
Reader.create(bytes)
)
return new ApplicationMetadataMessage(protoBuf)
}
encode(): Uint8Array {
return proto.ApplicationMetadataMessage.encode(this.proto).finish()
}
public get signature(): Uint8Array | undefined {
return this.proto.signature
}
public get payload(): Uint8Array | undefined {
return this.proto.payload
}
public get type(): ApplicationMetadataMessage_Type | undefined {
return this.proto.type
}
/**
* Returns a chat message if the type is [TYPE_CHAT_MESSAGE], undefined otherwise.
*/
public get chatMessage(): ChatMessage | undefined {
if (!this.payload) return
return ChatMessage.decode(this.payload)
}
public get signer(): Uint8Array | undefined {
if (!this.signature || !this.payload) return
const signature = this.signature.slice(0, 64)
const recid = this.signature.slice(64)[0]
const hash = keccak256(this.payload)
return secp256k1.ecdsaRecover(signature, recid, hexToBuf(hash))
}
}

View File

@ -1,52 +0,0 @@
import { Reader } from 'protobufjs'
import * as proto from '../proto/communities/v1/chat_identity'
import type { IdentityImage } from '../proto/communities/v1/chat_identity'
export class ChatIdentity {
public constructor(public proto: proto.ChatIdentity) {}
static decode(bytes: Uint8Array): ChatIdentity {
const protoBuf = proto.ChatIdentity.decode(Reader.create(bytes))
return new ChatIdentity(protoBuf)
}
encode(): Uint8Array {
return proto.ChatIdentity.encode(this.proto).finish()
}
/** Lamport timestamp of the message */
get clock(): number | undefined {
return this.proto.clock
}
/** ens_name is the valid ENS name associated with the chat key */
get ensName(): string | undefined {
return this.proto.ensName
}
/** images is a string indexed mapping of images associated with an identity */
get images(): { [key: string]: IdentityImage } | undefined {
return this.proto.images
}
/** display name is the user set identity, valid only for organisations */
get displayName(): string | undefined {
return this.proto.displayName
}
/** description is the user set description, valid only for organisations */
get description(): string | undefined {
return this.proto.description
}
get color(): string | undefined {
return this.proto.color
}
get emoji(): string | undefined {
return this.proto.emoji
}
}

View File

@ -1,93 +0,0 @@
import {
AudioMessage_AudioType,
ChatMessage_ContentType,
} from '../proto/communities/v1/chat_message'
import { ImageType } from '../proto/communities/v1/enums'
import { ChatMessage, ContentType } from './chat_message'
import type { AudioContent, ImageContent, StickerContent } from './chat_message'
describe('Chat Message', () => {
// todo:
// test('Encode & decode Text message', () => {
// const payload = Buffer.from([1, 1])
// const imageContent: ImageContent = {
// image: payload,
// imageType: ImageType.IMAGE_TYPE_PNG,
// contentType: ContentType.Image,
// }
// const message = ChatMessage.createMessage(1, 1, 'chat-id', imageContent)
// const buf = message.encode()
// const dec = ChatMessage.decode(buf)
// expect(dec.contentType).toEqual(ChatMessage_ContentType.CONTENT_TYPE_IMAGE)
// expect(dec.image?.payload?.toString()).toEqual(payload.toString())
// expect(dec.image?.type).toEqual(ImageType.IMAGE_TYPE_PNG)
// })
test('Encode & decode Image message', () => {
const payload = Buffer.from([1, 1])
const imageContent: ImageContent = {
image: payload,
imageType: ImageType.IMAGE_TYPE_PNG,
contentType: ContentType.Image,
}
const message = ChatMessage.createMessage(1, 1, 'chat-id', imageContent)
const buf = message.encode()
const dec = ChatMessage.decode(buf)
expect(dec.contentType).toEqual(ChatMessage_ContentType.CONTENT_TYPE_IMAGE)
expect(dec.image?.payload?.toString()).toEqual(payload.toString())
expect(dec.image?.type).toEqual(ImageType.IMAGE_TYPE_PNG)
})
test('Encode & decode Audio message', () => {
const payload = Buffer.from([1, 1])
const durationMs = 12345
const audioContent: AudioContent = {
audio: payload,
audioType: AudioMessage_AudioType.AUDIO_TYPE_AAC,
durationMs,
contentType: ContentType.Audio,
}
const message = ChatMessage.createMessage(1, 1, 'chat-id', audioContent)
const buf = message.encode()
const dec = ChatMessage.decode(buf)
expect(dec.contentType).toEqual(ChatMessage_ContentType.CONTENT_TYPE_AUDIO)
expect(dec.audio?.payload?.toString()).toEqual(payload.toString())
expect(dec.audio?.type).toEqual(ImageType.IMAGE_TYPE_PNG)
expect(dec.audio?.durationMs).toEqual(durationMs)
})
test('Encode & decode Sticker message', () => {
const hash = 'deadbeef'
const pack = 12345
const stickerContent: StickerContent = {
hash,
pack,
contentType: ContentType.Sticker,
}
const message = ChatMessage.createMessage(1, 1, 'chat-id', stickerContent)
const buf = message.encode()
const dec = ChatMessage.decode(buf)
expect(dec.contentType).toEqual(
ChatMessage_ContentType.CONTENT_TYPE_STICKER
)
expect(dec.sticker?.hash).toEqual(hash)
expect(dec.sticker?.pack).toEqual(pack)
})
})

View File

@ -1,224 +0,0 @@
import { Reader } from 'protobufjs'
import * as proto from '../proto/communities/v1/chat_message'
// import { proto.ChatMessage_ContentType } from '../proto/communities/v1/chat_message'
import { MessageType } from '../proto/communities/v1/enums'
import type {
AudioMessage,
AudioMessage_AudioType,
ImageMessage,
StickerMessage,
} from '../proto/communities/v1/chat_message'
import type { ImageType } from '../proto/communities/v1/enums'
export type Content = TextContent | StickerContent | ImageContent | AudioContent
export enum ContentType {
Text,
Sticker,
Image,
Audio,
}
export interface TextContent {
text: string
contentType: ContentType.Text
}
export interface StickerContent {
hash: string
pack: number
contentType: ContentType.Sticker
}
export interface ImageContent {
image: Uint8Array
imageType: ImageType
contentType: ContentType.Image
}
export interface AudioContent {
audio: Uint8Array
audioType: AudioMessage_AudioType
durationMs: number
contentType: ContentType.Audio
}
function isText(content: Content): content is TextContent {
return content.contentType === ContentType.Text
}
function isSticker(content: Content): content is StickerContent {
return content.contentType === ContentType.Sticker
}
function isImage(content: Content): content is ImageContent {
return content.contentType === ContentType.Image
}
function isAudio(content: Content): content is AudioContent {
return content.contentType === ContentType.Audio
}
export class ChatMessage {
private constructor(public _proto: proto.ChatMessage) {}
/**
* Create a chat message to be sent to an Open (permission = no membership) community.
*
* @throws string If mediaContent is malformed
*/
public static createMessage(
clock: number,
timestamp: number,
chatId: string,
content: Content,
responseTo?: string
): ChatMessage {
let sticker,
image,
audio,
text = 'Upgrade to the latest version to see this media content.'
let contentType = proto.ChatMessage_ContentType.CONTENT_TYPE_TEXT_PLAIN
if (isText(content)) {
if (!content.text) throw 'Malformed Text Content'
text = content.text
contentType = proto.ChatMessage_ContentType.CONTENT_TYPE_TEXT_PLAIN
} else if (isSticker(content)) {
if (!content.hash || !content.pack) throw 'Malformed Sticker Content'
sticker = {
hash: content.hash,
pack: content.pack,
}
contentType = proto.ChatMessage_ContentType.CONTENT_TYPE_STICKER
} else if (isImage(content)) {
if (!content.image || !content.imageType) throw 'Malformed Image Content'
image = {
payload: content.image,
type: content.imageType,
}
contentType = proto.ChatMessage_ContentType.CONTENT_TYPE_IMAGE
} else if (isAudio(content)) {
if (!content.audio || !content.audioType || !content.durationMs)
throw 'Malformed Audio Content'
audio = {
payload: content.audio,
type: content.audioType,
durationMs: content.durationMs,
}
contentType = proto.ChatMessage_ContentType.CONTENT_TYPE_AUDIO
}
const __proto = {
clock, // ms?
timestamp, //ms?
text,
/** Id of the message that we are replying to */
responseTo: responseTo ?? '',
/** Ens name of the sender */
ensName: '',
/** Public Key of the community (TBC) **/
chatId,
/** The type of message (public/one-to-one/private-group-chat) */
messageType: MessageType.MESSAGE_TYPE_COMMUNITY_CHAT,
/** The type of the content of the message */
contentType,
sticker,
image,
audio,
community: undefined, // Used to share a community
grant: undefined,
}
return new ChatMessage(__proto)
}
static decode(bytes: Uint8Array): ChatMessage {
const protoBuf = proto.ChatMessage.decode(Reader.create(bytes))
return new ChatMessage(protoBuf)
}
encode(): Uint8Array {
return proto.ChatMessage.encode(this._proto).finish()
}
/** Lamport timestamp of the chat message */
public get clock(): number | undefined {
return this._proto.clock
}
/**
* Unix timestamps in milliseconds, currently not used as we use whisper as more reliable, but here
* so that we don't rely on it
*/
public get timestamp(): number | undefined {
return this._proto.timestamp
}
/**
* Text of the message
*/
public get text(): string | undefined {
return this._proto.text
}
/**
* Id of the message that we are replying to
*/
public get responseTo(): string | undefined {
return this._proto.responseTo
}
/**
* Ens name of the sender
*/
public get ensName(): string | undefined {
return this._proto.ensName
}
/**
* Chat id, this field is symmetric for public-chats and private group chats,
* but asymmetric in case of one-to-ones, as the sender will use the chat-id
* of the received, while the receiver will use the chat-id of the sender.
* Probably should be the concatenation of sender-pk & receiver-pk in alphabetical order
*/
public get chatId(): string {
return this._proto.chatId
}
/**
* The type of message (public/one-to-one/private-group-chat)
*/
public get messageType(): MessageType | undefined {
return this._proto.messageType
}
/**
* The type of the content of the message
*/
public get contentType(): proto.ChatMessage_ContentType | undefined {
return this._proto.contentType
}
public get sticker(): StickerMessage | undefined {
return this._proto.sticker
}
public get image(): ImageMessage | undefined {
return this._proto.image
}
public get audio(): AudioMessage | undefined {
return this._proto.audio
}
/**
* Used when sharing a community via a chat message.
*/
public get community(): Uint8Array | undefined {
return this._proto.community
}
}

View File

@ -1,59 +0,0 @@
import { Reader } from 'protobufjs'
import * as proto from '../proto/communities/v1/communities'
import { ChatIdentity } from './chat_identity'
import type {
CommunityMember,
CommunityPermissions,
} from '../proto/communities/v1/communities'
export class CommunityChat {
public constructor(public proto: proto.CommunityChat) {}
/**
* Decode the payload as CommunityChat message.
*
* @throws
*/
static decode(bytes: Uint8Array): CommunityChat {
const protoBuf = proto.CommunityChat.decode(Reader.create(bytes))
return new CommunityChat(protoBuf)
}
encode(): Uint8Array {
return proto.CommunityChat.encode(this.proto).finish()
}
// TODO: check and document what is the key of the returned Map;
public get members(): Map<string, CommunityMember> {
const map = new Map()
for (const key of Object.keys(this.proto.members)) {
map.set(key, this.proto.members[key])
}
return map
}
public get permissions(): CommunityPermissions | undefined {
return this.proto.permissions
}
public get identity(): ChatIdentity | undefined {
if (!this.proto.identity) return
return new ChatIdentity(this.proto.identity)
}
// TODO: Document this
public get categoryId(): string | undefined {
return this.proto.categoryId
}
// TODO: Document this
public get position(): number | undefined {
return this.proto.position
}
}

View File

@ -1,103 +0,0 @@
import debug from 'debug'
import { Reader } from 'protobufjs'
import { idToContentTopic } from '../contentTopic'
import { createSymKeyFromPassword } from '../encryption'
// TODO: replace for 'packages/status-js/protos/communities.ts'
import * as proto from '../proto/communities/v1/communities'
import { bufToHex } from '../utils'
import { ApplicationMetadataMessage } from './application_metadata_message'
import { ChatIdentity } from './chat_identity'
import type { CommunityChat } from './community_chat'
import type { WakuMessage, WakuStore } from 'js-waku'
const dbg = debug('communities:wire:community_description')
export class CommunityDescription {
private constructor(public proto: proto.CommunityDescription) {}
static decode(bytes: Uint8Array): CommunityDescription {
const protoBuf = proto.CommunityDescription.decode(Reader.create(bytes))
return new CommunityDescription(protoBuf)
}
encode(): Uint8Array {
return proto.CommunityDescription.encode(this.proto).finish()
}
/**
* Retrieves the most recent Community Description it can find on the network.
*/
public static async retrieve(
communityPublicKey: Uint8Array,
wakuStore: WakuStore
): Promise<CommunityDescription | undefined> {
const hexCommunityPublicKey = bufToHex(communityPublicKey)
// TEST: diff topic
const contentTopic = idToContentTopic(hexCommunityPublicKey)
let communityDescription: CommunityDescription | undefined
const callback = (messages: WakuMessage[]): void => {
// Value found, stop processing
if (communityDescription) return
// Process most recent message first
const orderedMessages = messages.reverse()
orderedMessages.forEach((message: WakuMessage) => {
if (!message.payload) return
try {
const metadata = ApplicationMetadataMessage.decode(message.payload)
if (!metadata.payload) return
const _communityDescription = CommunityDescription.decode(
metadata.payload
)
if (!_communityDescription.identity) return
communityDescription = _communityDescription
} catch (e) {
dbg(
`Failed to decode message as CommunityDescription found on content topic ${contentTopic}`,
e
)
}
})
}
const symKey = await createSymKeyFromPassword(hexCommunityPublicKey)
await wakuStore
.queryHistory([contentTopic], {
callback,
decryptionKeys: [symKey],
})
.catch(e => {
dbg(
`Failed to retrieve community description for ${hexCommunityPublicKey}`,
e
)
})
return communityDescription
}
get identity(): ChatIdentity | undefined {
if (!this.proto.identity) return
return new ChatIdentity(this.proto.identity)
}
get chats(): Map<string, CommunityChat> {
const map = new Map()
for (const key of Object.keys(this.proto.chats)) {
map.set(key, this.proto.chats[key])
}
return map
}
}

View File

@ -1,201 +0,0 @@
import { keccak256 } from 'js-sha3'
import { Reader } from 'protobufjs'
import * as secp256k1 from 'secp256k1'
import { v4 as uuidV4 } from 'uuid'
import { Identity } from '..'
import * as proto from '../proto/communities/v1/membership_update_message'
import { bufToHex, hexToBuf } from '../utils'
export class MembershipUpdateEvent {
public constructor(public proto: proto.MembershipUpdateEvent) {}
static decode(bytes: Uint8Array): MembershipUpdateEvent {
const protoBuf = proto.MembershipUpdateEvent.decode(Reader.create(bytes))
return new MembershipUpdateEvent(protoBuf)
}
encode(): Uint8Array {
return proto.MembershipUpdateEvent.encode(this.proto).finish()
}
public get members(): string[] {
return this.proto.members
}
public get name(): string {
return this.proto.name
}
public get clock(): number {
return this.proto.clock
}
public get type(): proto.MembershipUpdateEvent_EventType {
return this.proto.type
}
}
export class MembershipSignedEvent {
public sig: Uint8Array
public event: MembershipUpdateEvent
private chatId: string
public constructor(
sig: Uint8Array,
event: MembershipUpdateEvent,
chatId: string
) {
this.sig = sig
this.event = event
this.chatId = chatId
}
public get signer(): Uint8Array | undefined {
const encEvent = this.event.encode()
const eventToSign = Buffer.concat([hexToBuf(this.chatId), encEvent])
if (!this.sig || !eventToSign) return
const signature = this.sig.slice(0, 64)
const recid = this.sig.slice(64)[0]
const hash = keccak256(eventToSign)
return secp256k1.ecdsaRecover(signature, recid, hexToBuf(hash))
}
}
export class MembershipUpdateMessage {
private clock: number = Date.now()
private identity: Identity = Identity.generate()
public constructor(public proto: proto.MembershipUpdateMessage) {}
public static create(
chatId: string,
identity: Identity
): MembershipUpdateMessage {
const partial = proto.MembershipUpdateMessage.fromPartial({
chatId,
events: [],
})
const newMessage = new MembershipUpdateMessage(partial)
newMessage.clock = Date.now()
newMessage.identity = identity
return newMessage
}
private addEvent(event: MembershipUpdateEvent): void {
const encEvent = event.encode()
const eventToSign = Buffer.concat([hexToBuf(this.proto.chatId), encEvent])
const signature = this.identity.sign(eventToSign)
this.proto.events.push(Buffer.concat([signature, encEvent]))
}
public static createChat(
identity: Identity,
members: string[],
name?: string
): MembershipUpdateMessage {
const chatId = `${uuidV4()}-${bufToHex(identity.publicKey)}`
const message = this.create(chatId, identity)
const type = proto.MembershipUpdateEvent_EventType.CHAT_CREATED
const event = new MembershipUpdateEvent({
clock: message.clock,
members,
name: name ?? '',
type,
})
message.addEvent(event)
return message
}
public addNameChangeEvent(name: string): void {
const type = proto.MembershipUpdateEvent_EventType.NAME_CHANGED
const event = new MembershipUpdateEvent({
clock: this.clock,
members: [],
name: name,
type,
})
this.addEvent(event)
}
public addMembersAddedEvent(members: string[]): void {
const type = proto.MembershipUpdateEvent_EventType.MEMBERS_ADDED
const event = new MembershipUpdateEvent({
clock: this.clock,
members,
name: '',
type,
})
this.addEvent(event)
}
public addMemberJoinedEvent(member: string): void {
const type = proto.MembershipUpdateEvent_EventType.MEMBER_JOINED
const event = new MembershipUpdateEvent({
clock: this.clock,
members: [member],
name: '',
type,
})
this.addEvent(event)
}
public addMemberRemovedEvent(member: string): void {
const type = proto.MembershipUpdateEvent_EventType.MEMBER_REMOVED
const event = new MembershipUpdateEvent({
clock: this.clock,
members: [member],
name: '',
type,
})
this.addEvent(event)
}
public addAdminsAddedEvent(members: string[]): void {
const type = proto.MembershipUpdateEvent_EventType.ADMINS_ADDED
const event = new MembershipUpdateEvent({
clock: this.clock,
members,
name: '',
type,
})
this.addEvent(event)
}
public addAdminRemovedEvent(member: string): void {
const type = proto.MembershipUpdateEvent_EventType.ADMINS_ADDED
const event = new MembershipUpdateEvent({
clock: this.clock,
members: [member],
name: '',
type,
})
this.addEvent(event)
}
static decode(bytes: Uint8Array): MembershipUpdateMessage {
const protoBuf = proto.MembershipUpdateMessage.decode(Reader.create(bytes))
return new MembershipUpdateMessage(protoBuf)
}
public get events(): MembershipSignedEvent[] {
return this.proto.events.map(bufArray => {
return new MembershipSignedEvent(
bufArray.slice(0, 65),
MembershipUpdateEvent.decode(bufArray.slice(65)),
this.chatId
)
})
}
public get chatId(): string {
return this.proto.chatId
}
encode(): Uint8Array {
return proto.MembershipUpdateMessage.encode(this.proto).finish()
}
}

View File

@ -1,49 +0,0 @@
import { Reader } from 'protobufjs'
import * as proto from '../proto/communities/v1/status_update'
export class StatusUpdate {
public constructor(public proto: proto.StatusUpdate) {}
public static create(
statusType: proto.StatusUpdate_StatusType,
customText: string
): StatusUpdate {
const clock = Date.now()
const proto = {
clock,
statusType,
customText,
}
return new StatusUpdate(proto)
}
/**
* Decode the payload as CommunityChat message.
*
* @throws
*/
static decode(bytes: Uint8Array): StatusUpdate {
const protoBuf = proto.StatusUpdate.decode(Reader.create(bytes))
return new StatusUpdate(protoBuf)
}
encode(): Uint8Array {
return proto.StatusUpdate.encode(this.proto).finish()
}
public get clock(): number | undefined {
return this.proto.clock
}
public get statusType(): proto.StatusUpdate_StatusType | undefined {
return this.proto.statusType
}
public get customText(): string | undefined {
return this.proto.customText
}
}

203
yarn.lock
View File

@ -1043,18 +1043,6 @@
protobufjs "^6.11.2" protobufjs "^6.11.2"
uint8arrays "^3.0.0" uint8arrays "^3.0.0"
"@cspotcode/source-map-consumer@0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b"
integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==
"@cspotcode/source-map-support@0.7.0":
version "0.7.0"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5"
integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==
dependencies:
"@cspotcode/source-map-consumer" "0.8.0"
"@emotion/is-prop-valid@^0.8.8": "@emotion/is-prop-valid@^0.8.8":
version "0.8.8" version "0.8.8"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a"
@ -3279,6 +3267,11 @@
resolved "https://registry.yarnpkg.com/@stitches/react/-/react-1.2.8.tgz#954f8008be8d9c65c4e58efa0937f32388ce3a38" resolved "https://registry.yarnpkg.com/@stitches/react/-/react-1.2.8.tgz#954f8008be8d9c65c4e58efa0937f32388ce3a38"
integrity sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA== integrity sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA==
"@swc/helpers@^0.2.11":
version "0.2.14"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.2.14.tgz#20288c3627442339dd3d743c944f7043ee3590f0"
integrity sha512-wpCQMhf5p5GhNg2MmGKXzUNwxe7zRiCsmqYsamez2beP7mKPCSiu+BjZcdN95yYSzO857kr0VfQewmGpS77nqA==
"@swc/helpers@^0.3.15": "@swc/helpers@^0.3.15":
version "0.3.16" version "0.3.16"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.3.16.tgz#896c44a5d476034d261f878bc4833da1624b1752" resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.3.16.tgz#896c44a5d476034d261f878bc4833da1624b1752"
@ -3291,26 +3284,6 @@
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
"@tsconfig/node10@^1.0.7":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9"
integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==
"@tsconfig/node12@^1.0.7":
version "1.0.9"
resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c"
integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==
"@tsconfig/node14@^1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2"
integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==
"@tsconfig/node16@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e"
integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==
"@types/babel__core@^7.1.14": "@types/babel__core@^7.1.14":
version "7.1.19" version "7.1.19"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.19.tgz#7b497495b7d1b4812bdb9d02804d0576f43ee460" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.19.tgz#7b497495b7d1b4812bdb9d02804d0576f43ee460"
@ -3344,13 +3317,6 @@
dependencies: dependencies:
"@babel/types" "^7.3.0" "@babel/types" "^7.3.0"
"@types/bn.js@*", "@types/bn.js@^5.1.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.0.tgz#32c5d271503a12653c62cf4d2b45e6eab8cebc68"
integrity sha512-QSSVYj7pYFN49kW77o2s9xTCwZ8F2xLbjLLSEVh8D2F4JUhZtPAGOFLTD+ffqksBx/u4cE/KImFjyhqCjn/LIA==
dependencies:
"@types/node" "*"
"@types/debug@^4.1.7": "@types/debug@^4.1.7":
version "4.1.7" version "4.1.7"
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
@ -3365,13 +3331,6 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/elliptic@^6.4.14":
version "6.4.14"
resolved "https://registry.yarnpkg.com/@types/elliptic/-/elliptic-6.4.14.tgz#7bbaad60567a588c1f08b10893453e6b9b4de48e"
integrity sha512-z4OBcDAU0GVwDTuwJzQCiL6188QvZMkvoERgcVjq0/mPM8jCfdwZ3x5zQEVoL9WCAru3aG5wl3Z5Ww5wBWn7ZQ==
dependencies:
"@types/bn.js" "*"
"@types/emoji-mart@^3.0.6": "@types/emoji-mart@^3.0.6":
version "3.0.9" version "3.0.9"
resolved "https://registry.yarnpkg.com/@types/emoji-mart/-/emoji-mart-3.0.9.tgz#2f7ef5d9ec194f28029c46c81a5fc1e5b0efa73c" resolved "https://registry.yarnpkg.com/@types/emoji-mart/-/emoji-mart-3.0.9.tgz#2f7ef5d9ec194f28029c46c81a5fc1e5b0efa73c"
@ -3446,11 +3405,6 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
"@types/lodash@^4.14.182":
version "4.14.182"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2"
integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==
"@types/long@^4.0.1": "@types/long@^4.0.1":
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
@ -3486,23 +3440,11 @@
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
"@types/object-hash@^1.3.0":
version "1.3.4"
resolved "https://registry.yarnpkg.com/@types/object-hash/-/object-hash-1.3.4.tgz#079ba142be65833293673254831b5e3e847fe58b"
integrity sha512-xFdpkAkikBgqBdG9vIlsqffDV8GpvnPEzs0IUtr1v3BEB97ijsFQ4RXVbUZwjFThhB4MDSTUfvmxUD5PGx0wXA==
"@types/parse-json@^4.0.0": "@types/parse-json@^4.0.0":
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
"@types/pbkdf2@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@types/pbkdf2/-/pbkdf2-3.1.0.tgz#039a0e9b67da0cdc4ee5dab865caa6b267bb66b1"
integrity sha512-Cf63Rv7jCQ0LaL8tNXmEyqTHuIJxRdlS5vMh1mj5voN4+QFhVZnlZruezqpWYDiJ8UTzhP0VmeLXCmBk66YrMQ==
dependencies:
"@types/node" "*"
"@types/prettier@^2.1.5": "@types/prettier@^2.1.5":
version "2.6.3" version "2.6.3"
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.6.3.tgz#68ada76827b0010d0db071f739314fa429943d0a" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.6.3.tgz#68ada76827b0010d0db071f739314fa429943d0a"
@ -3539,13 +3481,6 @@
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
"@types/secp256k1@^4.0.3":
version "4.0.3"
resolved "https://registry.yarnpkg.com/@types/secp256k1/-/secp256k1-4.0.3.tgz#1b8e55d8e00f08ee7220b4d59a6abe89c37a901c"
integrity sha512-Da66lEIFeIz9ltsdMZcpQvmrmmoqrfju8pm1BH8WbYjZSwUgCwXLb9C+9XYogwBITnbsSaMdVPb2ekf7TV+03w==
dependencies:
"@types/node" "*"
"@types/stack-utils@^2.0.0": "@types/stack-utils@^2.0.0":
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
@ -3560,11 +3495,6 @@
"@types/react" "*" "@types/react" "*"
csstype "^3.0.2" csstype "^3.0.2"
"@types/uuid@^8.3.3":
version "8.3.4"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
"@types/yargs-parser@*": "@types/yargs-parser@*":
version "20.2.1" version "20.2.1"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129"
@ -3686,12 +3616,7 @@ acorn-jsx@^5.3.1:
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
acorn-walk@^8.1.1: acorn@^8.5.0, acorn@^8.7.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.0:
version "8.7.0" version "8.7.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf"
integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==
@ -3773,11 +3698,6 @@ anymatch@^3.0.3:
normalize-path "^3.0.0" normalize-path "^3.0.0"
picomatch "^2.0.4" picomatch "^2.0.4"
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
argparse@^1.0.7: argparse@^1.0.7:
version "1.0.10" version "1.0.10"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
@ -4076,7 +3996,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9:
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
bn.js@^5.0.0, bn.js@^5.1.1, bn.js@^5.2.0: bn.js@^5.0.0, bn.js@^5.1.1:
version "5.2.0" version "5.2.0"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002"
integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw== integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==
@ -4516,11 +4436,6 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
sha.js "^2.4.8" sha.js "^2.4.8"
create-require@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
cross-spawn@^6.0.0, cross-spawn@^6.0.5: cross-spawn@^6.0.0, cross-spawn@^6.0.5:
version "6.0.5" version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@ -4681,11 +4596,6 @@ dashdash@^1.12.0:
dependencies: dependencies:
assert-plus "^1.0.0" assert-plus "^1.0.0"
dataloader@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.4.0.tgz#bca11d867f5d3f1b9ed9f737bd15970c65dff5c8"
integrity sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==
datastore-core@^7.0.0: datastore-core@^7.0.0:
version "7.0.1" version "7.0.1"
resolved "https://registry.yarnpkg.com/datastore-core/-/datastore-core-7.0.1.tgz#f50f30bb55474a569118d41bba6052896b096aec" resolved "https://registry.yarnpkg.com/datastore-core/-/datastore-core-7.0.1.tgz#f50f30bb55474a569118d41bba6052896b096aec"
@ -4814,11 +4724,6 @@ diff-sequences@^28.0.2:
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-28.0.2.tgz#40f8d4ffa081acbd8902ba35c798458d0ff1af41" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-28.0.2.tgz#40f8d4ffa081acbd8902ba35c798458d0ff1af41"
integrity sha512-YtEoNynLDFCRznv/XDalsKGSZDoj0U5kLnXvY0JSq3nBboRrZXjD81+eSiwi+nzcZDwedMmcowcxNwwgFW23mQ== integrity sha512-YtEoNynLDFCRznv/XDalsKGSZDoj0U5kLnXvY0JSq3nBboRrZXjD81+eSiwi+nzcZDwedMmcowcxNwwgFW23mQ==
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
diffie-hellman@^5.0.0: diffie-hellman@^5.0.0:
version "5.0.3" version "5.0.3"
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
@ -4935,14 +4840,6 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0" jsbn "~0.1.0"
safer-buffer "^2.1.0" safer-buffer "^2.1.0"
ecies-geth@^1.5.3:
version "1.6.3"
resolved "https://registry.yarnpkg.com/ecies-geth/-/ecies-geth-1.6.3.tgz#7b58434b6d7a4d93d1c54b5abe8974b5e911004a"
integrity sha512-RAZs5p0MZLGWXt3weAHjefnWzJwTDvMw8GizSHhPNM8HkGDkRnOjbJtN613BD+/EOPaTP5j7bwwd83WhJq+5Ew==
dependencies:
elliptic "^6.5.4"
secp256k1 "^4.0.3"
electron-fetch@^1.7.2: electron-fetch@^1.7.2:
version "1.7.4" version "1.7.4"
resolved "https://registry.yarnpkg.com/electron-fetch/-/electron-fetch-1.7.4.tgz#af975ab92a14798bfaa025f88dcd2e54a7b0b769" resolved "https://registry.yarnpkg.com/electron-fetch/-/electron-fetch-1.7.4.tgz#af975ab92a14798bfaa025f88dcd2e54a7b0b769"
@ -4960,7 +4857,7 @@ electron-to-chromium@^1.4.71:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.71.tgz#17056914465da0890ce00351a3b946fd4cd51ff6" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.71.tgz#17056914465da0890ce00351a3b946fd4cd51ff6"
integrity sha512-Hk61vXXKRb2cd3znPE9F+2pLWdIOmP7GjiTj45y6L3W/lO+hSnUSUhq+6lEaERWBdZOHbk2s3YV5c9xVl3boVw== integrity sha512-Hk61vXXKRb2cd3znPE9F+2pLWdIOmP7GjiTj45y6L3W/lO+hSnUSUhq+6lEaERWBdZOHbk2s3YV5c9xVl3boVw==
elliptic@^6.5.3, elliptic@^6.5.4: elliptic@^6.5.3:
version "6.5.4" version "6.5.4"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
@ -7352,7 +7249,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
lodash@^4.17.11, lodash@^4.17.15, lodash@^4.17.21, lodash@^4.17.4: lodash@^4.17.11, lodash@^4.17.4:
version "4.17.21" version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -7405,7 +7302,7 @@ make-dir@^3.0.0:
dependencies: dependencies:
semver "^6.0.0" semver "^6.0.0"
make-error@1.x, make-error@^1.1.1: make-error@1.x:
version "1.3.6" version "1.3.6"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
@ -7683,11 +7580,6 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
node-addon-api@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32"
integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==
node-addon-api@^3.2.1: node-addon-api@^3.2.1:
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
@ -7712,7 +7604,7 @@ node-gyp-build-optional-packages@^4.3.2:
resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-4.3.5.tgz#a1de0039f81ecacecefcbb4349cdb96842343b31" resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-4.3.5.tgz#a1de0039f81ecacecefcbb4349cdb96842343b31"
integrity sha512-5ke7D8SiQsTQL7CkHpfR1tLwfqtKc0KYEmlnkwd40jHCASskZeS98qoZ1qDUns2aUQWikcjidRUs6PM/3iyN/w== integrity sha512-5ke7D8SiQsTQL7CkHpfR1tLwfqtKc0KYEmlnkwd40jHCASskZeS98qoZ1qDUns2aUQWikcjidRUs6PM/3iyN/w==
node-gyp-build@^4.2.0, node-gyp-build@^4.2.3, node-gyp-build@^4.3.0: node-gyp-build@^4.2.3, node-gyp-build@^4.3.0:
version "4.3.0" version "4.3.0"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3"
integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==
@ -7813,11 +7705,6 @@ object-assign@^4.1.1:
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
object-hash@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df"
integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==
object-inspect@^1.11.0, object-inspect@^1.12.0, object-inspect@^1.9.0: object-inspect@^1.11.0, object-inspect@^1.12.0, object-inspect@^1.9.0:
version "1.12.0" version "1.12.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0"
@ -8181,7 +8068,7 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
pbkdf2@^3.0.3, pbkdf2@^3.1.2: pbkdf2@^3.0.3:
version "3.1.2" version "3.1.2"
resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075"
integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==
@ -9042,15 +8929,6 @@ scheduler@^0.20.2:
loose-envify "^1.1.0" loose-envify "^1.1.0"
object-assign "^4.1.1" object-assign "^4.1.1"
secp256k1@^4.0.2, secp256k1@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.3.tgz#c4559ecd1b8d3c1827ed2d1b94190d69ce267303"
integrity sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA==
dependencies:
elliptic "^6.5.4"
node-addon-api "^2.0.0"
node-gyp-build "^4.2.0"
"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.7.0, semver@^5.7.1: "semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.7.0, semver@^5.7.1:
version "5.7.1" version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
@ -9643,53 +9521,6 @@ ts-jest@^28.0.4:
semver "7.x" semver "7.x"
yargs-parser "^20.x" yargs-parser "^20.x"
ts-node@^10.2.1:
version "10.5.0"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.5.0.tgz#618bef5854c1fbbedf5e31465cbb224a1d524ef9"
integrity sha512-6kEJKwVxAJ35W4akuiysfKwKmjkbYxwQMTBaAxo9KKAx/Yd26mPUyhGz3ji+EsJoAgrLqVsYHNuuYwQe22lbtw==
dependencies:
"@cspotcode/source-map-support" "0.7.0"
"@tsconfig/node10" "^1.0.7"
"@tsconfig/node12" "^1.0.7"
"@tsconfig/node14" "^1.0.0"
"@tsconfig/node16" "^1.0.2"
acorn "^8.4.1"
acorn-walk "^8.1.1"
arg "^4.1.0"
create-require "^1.1.0"
diff "^4.0.1"
make-error "^1.1.1"
v8-compile-cache-lib "^3.0.0"
yn "3.1.1"
"ts-poet@^t 4.11.0":
version "4.11.0"
resolved "https://registry.yarnpkg.com/ts-poet/-/ts-poet-4.11.0.tgz#5566f499ec767920cc18b977624f084c52e8734a"
integrity sha512-OaXnCKsRs0yrc0O7LFhnq/US2DB4Wd313cS+qjG2XMksZ74pF/jvMHkJdURXJiAo4kSahL2N4e8JOdwUjOMNdw==
dependencies:
lodash "^4.17.15"
prettier "^2.5.1"
ts-proto-descriptors@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/ts-proto-descriptors/-/ts-proto-descriptors-1.6.0.tgz#ca6eafc882495a2e920da5b981d7b181b4e49c38"
integrity sha512-Vrhue2Ti99us/o76mGy28nF3W/Uanl1/8detyJw2yyRwiBC5yxy+hEZqQ/ZX2PbZ1vyCpJ51A9L4PnCCnkBMTQ==
dependencies:
long "^4.0.0"
protobufjs "^6.8.8"
ts-proto@^1.115.1:
version "1.115.1"
resolved "https://registry.yarnpkg.com/ts-proto/-/ts-proto-1.115.1.tgz#262d9506fe575e5d1a821397ae7f72df2ea8574d"
integrity sha512-Zq1TvLQdnD6eNhbfdccnk1X9tVN5bwwPX4n2/gR9rVEHApZwHInV5Ntqd9lzl22lJ2LjlCNNYQdMlWak8d3EGA==
dependencies:
"@types/object-hash" "^1.3.0"
dataloader "^1.4.0"
object-hash "^1.3.1"
protobufjs "^6.11.3"
ts-poet "^t 4.11.0"
ts-proto-descriptors "1.6.0"
tsconfig-paths@^3.12.0, tsconfig-paths@^3.9.0: tsconfig-paths@^3.12.0, tsconfig-paths@^3.9.0:
version "3.12.0" version "3.12.0"
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz#19769aca6ee8f6a1a341e38c8fa45dd9fb18899b" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz#19769aca6ee8f6a1a341e38c8fa45dd9fb18899b"
@ -9875,11 +9706,6 @@ uuid@^8.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
v8-compile-cache-lib@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz#0582bcb1c74f3a2ee46487ceecf372e46bce53e8"
integrity sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==
v8-compile-cache@^2.0.0, v8-compile-cache@^2.0.3: v8-compile-cache@^2.0.0, v8-compile-cache@^2.0.3:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
@ -10126,11 +9952,6 @@ yargs@^17.3.1:
y18n "^5.0.5" y18n "^5.0.5"
yargs-parser "^21.0.0" yargs-parser "^21.0.0"
yn@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
yocto-queue@^0.1.0: yocto-queue@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"