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:
parent
fed1dd210f
commit
36f448cb96
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"extension": ["ts"],
|
||||
"spec": "src/**/*.spec.ts",
|
||||
"require": "ts-node/register",
|
||||
"exit": true
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
# `status-js`
|
|
@ -38,30 +38,14 @@
|
|||
"proto:build": "buf generate"
|
||||
},
|
||||
"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",
|
||||
"js-sha3": "^0.8.0",
|
||||
"js-waku": "^0.23.0",
|
||||
"long": "^5.2.0",
|
||||
"pbkdf2": "^3.1.2",
|
||||
"protobufjs": "^6.11.3",
|
||||
"protons-runtime": "^1.0.4",
|
||||
"secp256k1": "^4.0.2",
|
||||
"uuid": "^8.3.2"
|
||||
"protons-runtime": "^1.0.4"
|
||||
},
|
||||
"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",
|
||||
"protons": "^3.0.4",
|
||||
"ts-node": "^10.2.1",
|
||||
"ts-proto": "^1.115.1"
|
||||
"protons": "^3.0.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -2,8 +2,8 @@ import { keccak256 } from 'ethereum-cryptography/keccak'
|
|||
import { getPublicKey, sign, utils } from 'ethereum-cryptography/secp256k1'
|
||||
import { bytesToHex, concatBytes } from 'ethereum-cryptography/utils'
|
||||
|
||||
import { compressPublicKey } from './utils/compress-public-key'
|
||||
import { generateUsername } from './utils/generate-username'
|
||||
import { compressPublicKey } from '../utils/compress-public-key'
|
||||
import { generateUsername } from '../utils/generate-username'
|
||||
|
||||
export class Account {
|
||||
public privateKey: string
|
|
@ -1,22 +1,24 @@
|
|||
import { PageDirection } from 'js-waku'
|
||||
|
||||
import {
|
||||
AudioMessage,
|
||||
ChatMessage as ChatMessageProto,
|
||||
DeleteMessage,
|
||||
EditMessage,
|
||||
ImageType,
|
||||
} from '~/protos/chat-message'
|
||||
import { EmojiReaction } from '~/protos/emoji-reaction'
|
||||
|
||||
import { idToContentTopic } from '../contentTopic'
|
||||
import { createSymKeyFromPassword } from '../encryption'
|
||||
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 type { MessageType } from '../../protos/enums'
|
||||
import type { Client } from '../client'
|
||||
import type { Client } from './client'
|
||||
import type { Community } from './community/community'
|
||||
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 { WakuMessage } from 'js-waku'
|
||||
|
||||
|
@ -89,7 +91,7 @@ export class Chat {
|
|||
) => {
|
||||
const id = `${community.publicKey}${uuid}`
|
||||
const contentTopic = idToContentTopic(id)
|
||||
const symmetricKey = await createSymKeyFromPassword(id)
|
||||
const symmetricKey = await generateKeyFromPassword(id)
|
||||
|
||||
return new Chat({
|
||||
client,
|
||||
|
@ -441,11 +443,11 @@ export class Chat {
|
|||
messageType: 'COMMUNITY_CHAT' as MessageType,
|
||||
sticker: { hash: '', pack: 0 },
|
||||
image: {
|
||||
type: 'JPEG',
|
||||
type: ImageType.JPEG,
|
||||
payload: new Uint8Array([]),
|
||||
},
|
||||
audio: {
|
||||
type: 'AAC',
|
||||
type: AudioMessage.AudioType.AAC,
|
||||
payload: new Uint8Array([]),
|
||||
durationMs: BigInt(0),
|
||||
},
|
||||
|
@ -478,7 +480,7 @@ export class Chat {
|
|||
payload: image.payload,
|
||||
},
|
||||
audio: {
|
||||
type: 'AAC',
|
||||
type: AudioMessage.AudioType.AAC,
|
||||
payload: new Uint8Array([]),
|
||||
durationMs: BigInt(0),
|
||||
},
|
||||
|
|
|
@ -8,8 +8,8 @@ import { Waku, WakuMessage } from 'js-waku'
|
|||
import { ApplicationMetadataMessage } from '~/protos/application-metadata-message'
|
||||
|
||||
import { Account } from './account'
|
||||
import { Community } from './client/community/community'
|
||||
import { handleWakuMessage } from './client/community/handle-waku-message'
|
||||
import { Community } from './community/community'
|
||||
import { handleWakuMessage } from './community/handle-waku-message'
|
||||
|
||||
export interface ClientOptions {
|
||||
publicKey: string
|
|
@ -6,13 +6,13 @@ import { MessageType } from '~/protos/enums'
|
|||
import { getDifferenceByKeys } from '~/src/helpers/get-difference-by-keys'
|
||||
import { getObjectsDifference } from '~/src/helpers/get-objects-difference'
|
||||
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 { Member } from '../member'
|
||||
|
||||
import type { Client } from '../../client'
|
||||
import type { Client } from '../client'
|
||||
import type {
|
||||
CommunityChat,
|
||||
CommunityDescription,
|
||||
|
@ -44,7 +44,7 @@ export class Community {
|
|||
|
||||
public async start() {
|
||||
this.contentTopic = idToContentTopic(this.publicKey)
|
||||
this.symmetricKey = await createSymKeyFromPassword(this.publicKey)
|
||||
this.symmetricKey = await generateKeyFromPassword(this.publicKey)
|
||||
|
||||
// Waku
|
||||
this.client.waku.store.addDecryptionKey(this.symmetricKey, {
|
||||
|
@ -147,14 +147,24 @@ export class Community {
|
|||
private unobserveChatMessages = (
|
||||
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 contentTopic = chat!.contentTopic
|
||||
|
||||
if (!chat) {
|
||||
continue
|
||||
}
|
||||
|
||||
const contentTopic = chat.contentTopic
|
||||
|
||||
this.chats.delete(chatUuid)
|
||||
contentTopics.push(contentTopic)
|
||||
}
|
||||
|
||||
return contentTopic
|
||||
})
|
||||
if (!contentTopics.length) {
|
||||
return
|
||||
}
|
||||
|
||||
this.client.waku.relay.deleteObserver(
|
||||
this.client.handleWakuMessage,
|
||||
|
|
|
@ -16,7 +16,7 @@ import { recoverPublicKey } from '../../utils/recover-public-key'
|
|||
import { getChatUuid } from './get-chat-uuid'
|
||||
import { mapChatMessage } from './map-chat-message'
|
||||
|
||||
import type { Client } from '../../client'
|
||||
import type { Client } from '../client'
|
||||
import type { Community } from './community'
|
||||
import type { WakuMessage } from 'js-waku'
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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')
|
||||
}
|
|
@ -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'
|
||||
)
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
export type { Account } from './account'
|
||||
export type { Client, ClientOptions } from './client'
|
||||
export { createClient } from './client'
|
||||
export type { Account } from './client/account'
|
||||
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 { Member } from './client/member'
|
||||
|
|
|
@ -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() {}
|
||||
}
|
|
@ -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()
|
||||
})
|
||||
})
|
|
@ -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 !== ''
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -4,7 +4,7 @@ import { generateKeyFromPassword } from './generate-key-from-password'
|
|||
|
||||
describe('createSymKeyFromPassword', () => {
|
||||
it('should create symmetric key from password', async () => {
|
||||
const password = 'password'
|
||||
const password = 'arbitrary data here'
|
||||
const symKey = await generateKeyFromPassword(password)
|
||||
|
||||
expect(bytesToHex(symKey)).toEqual(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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
|
||||
|
||||
|
@ -10,7 +10,7 @@ export async function generateKeyFromPassword(
|
|||
password: string
|
||||
): Promise<Uint8Array> {
|
||||
return await pbkdf2(
|
||||
hexToBytes(password),
|
||||
utf8ToBytes(password),
|
||||
utf8ToBytes(''),
|
||||
65356,
|
||||
AES_KEY_LENGTH,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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
|
||||
|
@ -9,8 +9,8 @@ import { bytesToHex, hexToBytes } from 'ethereum-cryptography/utils'
|
|||
const TOPIC_LENGTH = 4
|
||||
|
||||
export function idToContentTopic(id: string): string {
|
||||
const hash = keccak256(hexToBytes(id))
|
||||
const hash = keccak256(utf8ToBytes(id))
|
||||
const topic = hash.slice(0, TOPIC_LENGTH)
|
||||
|
||||
return `/waku/1/${bytesToHex(topic)}/rfc26`
|
||||
return `/waku/1/0x${bytesToHex(topic)}/rfc26`
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { bytesToHex, utf8ToBytes } from 'ethereum-cryptography/utils'
|
||||
|
||||
import { Account } from '../account'
|
||||
import { Account } from '../client/account'
|
||||
import { recoverPublicKey } from './recover-public-key'
|
||||
|
||||
import type { ApplicationMetadataMessage } from '~/protos/application-metadata-message'
|
||||
import type { ApplicationMetadataMessage } from '../../protos/application-metadata-message'
|
||||
|
||||
describe('recoverPublicKey', () => {
|
||||
it('should recover public key', async () => {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
203
yarn.lock
|
@ -1043,18 +1043,6 @@
|
|||
protobufjs "^6.11.2"
|
||||
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":
|
||||
version "0.8.8"
|
||||
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"
|
||||
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":
|
||||
version "0.3.16"
|
||||
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"
|
||||
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":
|
||||
version "7.1.19"
|
||||
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.19.tgz#7b497495b7d1b4812bdb9d02804d0576f43ee460"
|
||||
|
@ -3344,13 +3317,6 @@
|
|||
dependencies:
|
||||
"@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":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
|
||||
|
@ -3365,13 +3331,6 @@
|
|||
dependencies:
|
||||
"@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":
|
||||
version "3.0.9"
|
||||
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"
|
||||
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":
|
||||
version "4.0.1"
|
||||
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"
|
||||
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":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||
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":
|
||||
version "2.6.3"
|
||||
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"
|
||||
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":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
|
||||
|
@ -3560,11 +3495,6 @@
|
|||
"@types/react" "*"
|
||||
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@*":
|
||||
version "20.2.1"
|
||||
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"
|
||||
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
||||
|
||||
acorn-walk@^8.1.1:
|
||||
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:
|
||||
acorn@^8.5.0, acorn@^8.7.0:
|
||||
version "8.7.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf"
|
||||
integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==
|
||||
|
@ -3773,11 +3698,6 @@ anymatch@^3.0.3:
|
|||
normalize-path "^3.0.0"
|
||||
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:
|
||||
version "1.0.10"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002"
|
||||
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"
|
||||
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:
|
||||
version "6.0.5"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
|
||||
|
@ -4681,11 +4596,6 @@ dashdash@^1.12.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "7.0.1"
|
||||
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"
|
||||
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:
|
||||
version "5.0.3"
|
||||
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"
|
||||
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:
|
||||
version "1.7.4"
|
||||
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"
|
||||
integrity sha512-Hk61vXXKRb2cd3znPE9F+2pLWdIOmP7GjiTj45y6L3W/lO+hSnUSUhq+6lEaERWBdZOHbk2s3YV5c9xVl3boVw==
|
||||
|
||||
elliptic@^6.5.3, elliptic@^6.5.4:
|
||||
elliptic@^6.5.3:
|
||||
version "6.5.4"
|
||||
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
@ -7405,7 +7302,7 @@ make-dir@^3.0.0:
|
|||
dependencies:
|
||||
semver "^6.0.0"
|
||||
|
||||
make-error@1.x, make-error@^1.1.1:
|
||||
make-error@1.x:
|
||||
version "1.3.6"
|
||||
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
|
||||
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"
|
||||
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:
|
||||
version "3.2.1"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3"
|
||||
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"
|
||||
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:
|
||||
version "1.12.0"
|
||||
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"
|
||||
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
|
||||
|
||||
pbkdf2@^3.0.3, pbkdf2@^3.1.2:
|
||||
pbkdf2@^3.0.3:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075"
|
||||
integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==
|
||||
|
@ -9042,15 +8929,6 @@ scheduler@^0.20.2:
|
|||
loose-envify "^1.1.0"
|
||||
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:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||
|
@ -9643,53 +9521,6 @@ ts-jest@^28.0.4:
|
|||
semver "7.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:
|
||||
version "3.12.0"
|
||||
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"
|
||||
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:
|
||||
version "2.3.0"
|
||||
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"
|
||||
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:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||
|
|
Loading…
Reference in New Issue