Skip to content

Commit

Permalink
Merge pull request #86 from xmtp/jazzz/keybundle-network-store
Browse files Browse the repository at this point in the history
Keybundle network store
  • Loading branch information
jazzz authored Mar 15, 2022
2 parents 58abd77 + 35aa82b commit 0355f56
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 5 deletions.
26 changes: 21 additions & 5 deletions src/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { sleep } from '../test/helpers'
import Stream, { messageStream } from './Stream'
import { Signer } from 'ethers'
import { EncryptedStore, LocalStorageStore } from './store'
import { EncryptedStore, LocalStorageStore, PrivateTopicStore } from './store'
import { Conversations } from './conversations'

const NODES_LIST_URL = 'https://nodes.xmtp.com/'
Expand Down Expand Up @@ -81,7 +81,8 @@ export default class Client {
*/
static async create(wallet: Signer, opts?: CreateOptions): Promise<Client> {
const waku = await createWaku(opts || {})
const keys = await loadOrCreateKeys(wallet)
const keyStore = createNetworkPrivateKeyStore(wallet, waku)
const keys = await loadOrCreateKeys(wallet, keyStore)
const client = new Client(waku, keys)
await client.publishUserContact()
return client
Expand Down Expand Up @@ -262,10 +263,25 @@ export default class Client {
}
}

// Create Encrypted store which uses the Network to store KeyBundles
function createNetworkPrivateKeyStore(
wallet: Signer,
waku: Waku
): EncryptedStore {
return new EncryptedStore(wallet, new PrivateTopicStore(waku))
}

// Create Encrypted store which uses LocalStorage to store KeyBundles
function createLocalPrivateKeyStore(wallet: Signer): EncryptedStore {
return new EncryptedStore(wallet, new LocalStorageStore())
}

// attempt to load pre-existing key bundle from storage,
// otherwise create new key-bundle, store it and return it
async function loadOrCreateKeys(wallet: Signer): Promise<PrivateKeyBundle> {
const store = new EncryptedStore(wallet, new LocalStorageStore())
async function loadOrCreateKeys(
wallet: Signer,
store: EncryptedStore
): Promise<PrivateKeyBundle> {
let keys = await store.loadPrivateKeyBundle()
if (keys) {
return keys
Expand All @@ -276,7 +292,7 @@ async function loadOrCreateKeys(wallet: Signer): Promise<PrivateKeyBundle> {
}

// initialize connection to the network
async function createWaku({
export async function createWaku({
bootstrapAddrs,
env = 'testnet',
waitForPeersTimeoutMs,
Expand Down
39 changes: 39 additions & 0 deletions src/store/PrivateTopicStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// This creates an interface for storing data to the storage network.
import { Store } from './Store'
import { Waku, WakuMessage, PageDirection } from 'js-waku'
import { buildUserPrivateStoreTopic } from '../utils'

export default class NetworkStore implements Store {
private waku: Waku

constructor(waku: Waku) {
this.waku = waku
}

// Returns the first record in a topic if it is present.
async get(key: string): Promise<Buffer | null> {
const contents = (
await this.waku.store.queryHistory([buildUserPrivateStoreTopic(key)], {
pageSize: 1,
pageDirection: PageDirection.FORWARD,
callback: function (msgs: WakuMessage[]): boolean {
return Boolean(msgs[0].payload)
},
})
)
.filter((msg: WakuMessage) => msg.payload)
.map((msg: WakuMessage) => msg.payload as Uint8Array)
return contents.length > 0 ? Buffer.from(contents[0]) : null
}

async set(key: string, value: Buffer): Promise<void> {
const keys = Uint8Array.from(value)
await this.waku.relay.send(
await WakuMessage.fromBytes(keys, this.buildTopic(key))
)
}

private buildTopic(key: string): string {
return buildUserPrivateStoreTopic(key)
}
}
1 change: 1 addition & 0 deletions src/store/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as LocalStorageStore } from './LocalStorageStore'
export { default as EncryptedStore } from './EncryptedStore'
export { default as PrivateTopicStore } from './PrivateTopicStore'
export { Store } from './Store'
4 changes: 4 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export const buildUserIntroTopic = (walletAddr: string): string => {
return buildContentTopic(`intro-${walletAddr}`)
}

export const buildUserPrivateStoreTopic = (walletAddr: string): string => {
return buildContentTopic(`privatestore-${walletAddr}`)
}

export const sleep = (ms: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, ms))

Expand Down
94 changes: 94 additions & 0 deletions test/store/NetworkStore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Wallet } from 'ethers'
import { Waku } from 'js-waku'

import { newWallet, sleep } from '../helpers'
import { createWaku } from '../../src/Client'
import { PrivateTopicStore } from '../../src/store'

const newLocalDockerWaku = (): Promise<Waku> =>
createWaku({
bootstrapAddrs: [
'/ip4/127.0.0.1/tcp/9001/ws/p2p/16Uiu2HAmNCxLZCkXNbpVPBpSSnHj9iq4HZQj7fxRzw2kj1kKSHHA',
],
})

const newTestnetWaku = (): Promise<Waku> => createWaku({ env: 'testnet' })

describe('PrivateTopicStore', () => {
jest.setTimeout(10000)
const tests = [
{
name: 'local docker node',
newWaku: newLocalDockerWaku,
},
]
if (process.env.CI || process.env.TESTNET) {
tests.push({
name: 'testnet',
newWaku: newTestnetWaku,
})
}
tests.forEach((testCase) => {
describe(testCase.name, () => {
let waku: Waku
let wallet: Wallet
let store: PrivateTopicStore
beforeAll(async () => {
waku = await testCase.newWaku()
})
afterAll(async () => {
if (waku) await waku.stop()
})

beforeEach(async () => {
wallet = newWallet()
store = new PrivateTopicStore(waku)
})

it('roundtrip', async () => {
const key = wallet.address

const value = new TextEncoder().encode('hello')
const empty = await store.get(key)
expect(empty).toBeNull()

await store.set(key, Buffer.from(value))
const full = await store.get(key)

expect(full).toBeDefined()
expect(full).toEqual(Buffer.from(value))
})

it('distinct topics', async () => {
const valueA = Buffer.from(new TextEncoder().encode('helloA'))
const valueB = Buffer.from(new TextEncoder().encode('helloB'))
const keyA = wallet.address + 'A'
const keyB = wallet.address + 'B'

store.set(keyA, valueA)
store.set(keyB, valueB)
const responseA = await store.get(keyA)
const responseB = await store.get(keyB)

expect(responseA).toEqual(valueA)
expect(responseB).toEqual(valueB)
expect(responseA).not.toEqual(responseB)
})

it('over write safety', async () => {
const key = wallet.address

const firstValue = new TextEncoder().encode('a')
const secondValue = new TextEncoder().encode('bb')

await store.set(key, Buffer.from(firstValue))
await sleep(10) // Add wait to enforce a consistent order of messages
await store.set(key, Buffer.from(secondValue))
await sleep(10) // Add wait to enforce a consistent order of messages
const returnedValue = await store.get(key)

expect(returnedValue).toEqual(Buffer.from(firstValue))
})
})
})
})

0 comments on commit 0355f56

Please sign in to comment.