From 2e5d1186008ca420454ad7496054d861cc1532e7 Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Tue, 5 Apr 2022 23:08:38 +0400 Subject: [PATCH 01/10] Change keypair type in ecdh encryption util --- src/utils/ecdh-encryption.spec.ts | 44 +++++++++++++------------------ src/utils/ecdh-encryption.ts | 13 +++------ 2 files changed, 23 insertions(+), 34 deletions(-) diff --git a/src/utils/ecdh-encryption.spec.ts b/src/utils/ecdh-encryption.spec.ts index 43d8117..16a2986 100644 --- a/src/utils/ecdh-encryption.spec.ts +++ b/src/utils/ecdh-encryption.spec.ts @@ -10,19 +10,13 @@ import { NONCE_SIZE_BYTES } from './nonce-generator'; import { Keypair } from '@solana/web3.js'; import ed2curve from 'ed2curve'; -function generateKeypair() { +function generateCurve25519Keypair() { const { publicKey, secretKey } = new Keypair(); - const curve25519: Curve25519KeyPair = ed2curve.convertKeyPair({ + const keyPair: Curve25519KeyPair = ed2curve.convertKeyPair({ publicKey: publicKey.toBytes(), secretKey, })!; - return { - ed25519: { - publicKey: publicKey.toBytes(), - secretKey, - }, - curve25519, - }; + return keyPair; } describe('ECDH encryptor/decryptor test', async () => { @@ -42,13 +36,13 @@ describe('ECDH encryptor/decryptor test', async () => { const sizesComparison = messageSizes.map((size) => { const unencrypted = randomBytes(size); const nonce = randomBytes(NONCE_SIZE_BYTES); - const keyPair1 = generateKeypair(); - const keyPair2 = generateKeypair(); + const keyPair1 = generateCurve25519Keypair(); + const keyPair2 = generateCurve25519Keypair(); const encrypted = ecdhEncrypt( unencrypted, - keyPair1.curve25519, - keyPair2.ed25519.publicKey, + keyPair1, + keyPair2.publicKey, nonce, ); return { @@ -67,19 +61,19 @@ describe('ECDH encryptor/decryptor test', async () => { // given const unencrypted = randomBytes(10); const nonce = randomBytes(NONCE_SIZE_BYTES); - const party1KeyPair = generateKeypair(); - const party2KeyPair = generateKeypair(); + const party1KeyPair = generateCurve25519Keypair(); + const party2KeyPair = generateCurve25519Keypair(); const encrypted = ecdhEncrypt( unencrypted, - party1KeyPair.curve25519, - party2KeyPair.ed25519.publicKey, + party1KeyPair, + party2KeyPair.publicKey, nonce, ); // when const decrypted = ecdhDecrypt( encrypted, - party1KeyPair.curve25519, - party2KeyPair.ed25519.publicKey, + party1KeyPair, + party2KeyPair.publicKey, nonce, ); // then @@ -91,19 +85,19 @@ describe('ECDH encryptor/decryptor test', async () => { // given const unencrypted = randomBytes(10); const nonce = randomBytes(NONCE_SIZE_BYTES); - const party1KeyPair = generateKeypair(); - const party2KeyPair = generateKeypair(); + const party1KeyPair = generateCurve25519Keypair(); + const party2KeyPair = generateCurve25519Keypair(); const encrypted = ecdhEncrypt( unencrypted, - party1KeyPair.curve25519, - party2KeyPair.ed25519.publicKey, + party1KeyPair, + party2KeyPair.publicKey, nonce, ); // when const decrypted = ecdhDecrypt( encrypted, - party2KeyPair.curve25519, - party1KeyPair.ed25519.publicKey, + party2KeyPair, + party1KeyPair.publicKey, nonce, ); // then diff --git a/src/utils/ecdh-encryption.ts b/src/utils/ecdh-encryption.ts index bf5cf3f..57724b8 100644 --- a/src/utils/ecdh-encryption.ts +++ b/src/utils/ecdh-encryption.ts @@ -54,27 +54,22 @@ export function ed25519PublicKeyToCurve25519(key: Ed25519Key): Curve25519Key { export function ecdhEncrypt( payload: Uint8Array, { secretKey, publicKey }: Curve25519KeyPair, - otherPartyPublicKey: Ed25519Key, + otherPartyPublicKey: Curve25519Key, nonce: Uint8Array, ): Uint8Array { - return nacl.box( - payload, - nonce, - ed25519PublicKeyToCurve25519(otherPartyPublicKey), - secretKey, - ); + return nacl.box(payload, nonce, otherPartyPublicKey, secretKey); } export function ecdhDecrypt( payload: Uint8Array, { secretKey, publicKey }: Curve25519KeyPair, - otherPartyPublicKey: Ed25519Key, + otherPartyPublicKey: Curve25519Key, nonce: Uint8Array, ): Uint8Array { const decrypted = nacl.box.open( payload, nonce, - ed25519PublicKeyToCurve25519(otherPartyPublicKey), + otherPartyPublicKey, secretKey, ); if (!decrypted) { From 8a14f0fb488bcc6d55d20a04f675141ae5d7be8b Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Tue, 5 Apr 2022 23:45:48 +0400 Subject: [PATCH 02/10] Use Ed25519Key in ecdh utils to represent other party --- src/utils/ecdh-encryption.spec.ts | 44 ++++++++++++++++++------------- src/utils/ecdh-encryption.ts | 13 ++++++--- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/src/utils/ecdh-encryption.spec.ts b/src/utils/ecdh-encryption.spec.ts index 16a2986..43d8117 100644 --- a/src/utils/ecdh-encryption.spec.ts +++ b/src/utils/ecdh-encryption.spec.ts @@ -10,13 +10,19 @@ import { NONCE_SIZE_BYTES } from './nonce-generator'; import { Keypair } from '@solana/web3.js'; import ed2curve from 'ed2curve'; -function generateCurve25519Keypair() { +function generateKeypair() { const { publicKey, secretKey } = new Keypair(); - const keyPair: Curve25519KeyPair = ed2curve.convertKeyPair({ + const curve25519: Curve25519KeyPair = ed2curve.convertKeyPair({ publicKey: publicKey.toBytes(), secretKey, })!; - return keyPair; + return { + ed25519: { + publicKey: publicKey.toBytes(), + secretKey, + }, + curve25519, + }; } describe('ECDH encryptor/decryptor test', async () => { @@ -36,13 +42,13 @@ describe('ECDH encryptor/decryptor test', async () => { const sizesComparison = messageSizes.map((size) => { const unencrypted = randomBytes(size); const nonce = randomBytes(NONCE_SIZE_BYTES); - const keyPair1 = generateCurve25519Keypair(); - const keyPair2 = generateCurve25519Keypair(); + const keyPair1 = generateKeypair(); + const keyPair2 = generateKeypair(); const encrypted = ecdhEncrypt( unencrypted, - keyPair1, - keyPair2.publicKey, + keyPair1.curve25519, + keyPair2.ed25519.publicKey, nonce, ); return { @@ -61,19 +67,19 @@ describe('ECDH encryptor/decryptor test', async () => { // given const unencrypted = randomBytes(10); const nonce = randomBytes(NONCE_SIZE_BYTES); - const party1KeyPair = generateCurve25519Keypair(); - const party2KeyPair = generateCurve25519Keypair(); + const party1KeyPair = generateKeypair(); + const party2KeyPair = generateKeypair(); const encrypted = ecdhEncrypt( unencrypted, - party1KeyPair, - party2KeyPair.publicKey, + party1KeyPair.curve25519, + party2KeyPair.ed25519.publicKey, nonce, ); // when const decrypted = ecdhDecrypt( encrypted, - party1KeyPair, - party2KeyPair.publicKey, + party1KeyPair.curve25519, + party2KeyPair.ed25519.publicKey, nonce, ); // then @@ -85,19 +91,19 @@ describe('ECDH encryptor/decryptor test', async () => { // given const unencrypted = randomBytes(10); const nonce = randomBytes(NONCE_SIZE_BYTES); - const party1KeyPair = generateCurve25519Keypair(); - const party2KeyPair = generateCurve25519Keypair(); + const party1KeyPair = generateKeypair(); + const party2KeyPair = generateKeypair(); const encrypted = ecdhEncrypt( unencrypted, - party1KeyPair, - party2KeyPair.publicKey, + party1KeyPair.curve25519, + party2KeyPair.ed25519.publicKey, nonce, ); // when const decrypted = ecdhDecrypt( encrypted, - party2KeyPair, - party1KeyPair.publicKey, + party2KeyPair.curve25519, + party1KeyPair.ed25519.publicKey, nonce, ); // then diff --git a/src/utils/ecdh-encryption.ts b/src/utils/ecdh-encryption.ts index 57724b8..bf5cf3f 100644 --- a/src/utils/ecdh-encryption.ts +++ b/src/utils/ecdh-encryption.ts @@ -54,22 +54,27 @@ export function ed25519PublicKeyToCurve25519(key: Ed25519Key): Curve25519Key { export function ecdhEncrypt( payload: Uint8Array, { secretKey, publicKey }: Curve25519KeyPair, - otherPartyPublicKey: Curve25519Key, + otherPartyPublicKey: Ed25519Key, nonce: Uint8Array, ): Uint8Array { - return nacl.box(payload, nonce, otherPartyPublicKey, secretKey); + return nacl.box( + payload, + nonce, + ed25519PublicKeyToCurve25519(otherPartyPublicKey), + secretKey, + ); } export function ecdhDecrypt( payload: Uint8Array, { secretKey, publicKey }: Curve25519KeyPair, - otherPartyPublicKey: Curve25519Key, + otherPartyPublicKey: Ed25519Key, nonce: Uint8Array, ): Uint8Array { const decrypted = nacl.box.open( payload, nonce, - otherPartyPublicKey, + ed25519PublicKeyToCurve25519(otherPartyPublicKey), secretKey, ); if (!decrypted) { From 9adf5d59e038fb1e50d53a07d259b5fb5a29f19a Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Tue, 5 Apr 2022 23:55:57 +0400 Subject: [PATCH 03/10] Add encryption props to text serde --- src/api/text-serde.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/api/text-serde.ts b/src/api/text-serde.ts index fff8dda..d4b8a0c 100644 --- a/src/api/text-serde.ts +++ b/src/api/text-serde.ts @@ -31,11 +31,11 @@ export class EncryptedTextSerde implements TextSerde { const encryptionNonce = bytes.slice(0, NONCE_SIZE_BYTES); const encryptedText = bytes.slice(NONCE_SIZE_BYTES, bytes.length); const otherMember = this.findOtherMember( - new PublicKey(this.encryptionProps.ed25519PublicKey), + new PublicKey(this.encryptionProps.publicKey), ); const encodedText = ecdhDecrypt( encryptedText, - this.encryptionProps.diffieHellmanKeyPair, + this.encryptionProps.keypair, otherMember.publicKey.toBytes(), encryptionNonce, ); @@ -43,14 +43,14 @@ export class EncryptedTextSerde implements TextSerde { } serialize(text: string): Uint8Array { - const publicKey = new PublicKey(this.encryptionProps.ed25519PublicKey); + const publicKey = new PublicKey(this.encryptionProps.publicKey); const senderMemberIdx = this.findMemberIdx(publicKey); const textBytes = this.unencryptedTextSerde.serialize(text); const otherMember = this.findOtherMember(publicKey); const encryptionNonce = generateRandomNonceWithPrefix(senderMemberIdx); const encryptedText = ecdhEncrypt( textBytes, - this.encryptionProps.diffieHellmanKeyPair, + this.encryptionProps.keypair, otherMember.publicKey.toBytes(), encryptionNonce, @@ -93,8 +93,8 @@ export type DialectAttributes = { }; export interface EncryptionProps { - diffieHellmanKeyPair: Curve25519KeyPair; - ed25519PublicKey: Ed25519Key; + keypair: Curve25519KeyPair; + publicKey: Ed25519Key; } export class TextSerdeFactory { From 4acc40cc0f5cf1e5085a72a1d0e990431ed54a94 Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Wed, 6 Apr 2022 19:26:04 +0400 Subject: [PATCH 04/10] Fix tests after switching to dh keypair --- src/api/text-serde.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/api/text-serde.ts b/src/api/text-serde.ts index d4b8a0c..fff8dda 100644 --- a/src/api/text-serde.ts +++ b/src/api/text-serde.ts @@ -31,11 +31,11 @@ export class EncryptedTextSerde implements TextSerde { const encryptionNonce = bytes.slice(0, NONCE_SIZE_BYTES); const encryptedText = bytes.slice(NONCE_SIZE_BYTES, bytes.length); const otherMember = this.findOtherMember( - new PublicKey(this.encryptionProps.publicKey), + new PublicKey(this.encryptionProps.ed25519PublicKey), ); const encodedText = ecdhDecrypt( encryptedText, - this.encryptionProps.keypair, + this.encryptionProps.diffieHellmanKeyPair, otherMember.publicKey.toBytes(), encryptionNonce, ); @@ -43,14 +43,14 @@ export class EncryptedTextSerde implements TextSerde { } serialize(text: string): Uint8Array { - const publicKey = new PublicKey(this.encryptionProps.publicKey); + const publicKey = new PublicKey(this.encryptionProps.ed25519PublicKey); const senderMemberIdx = this.findMemberIdx(publicKey); const textBytes = this.unencryptedTextSerde.serialize(text); const otherMember = this.findOtherMember(publicKey); const encryptionNonce = generateRandomNonceWithPrefix(senderMemberIdx); const encryptedText = ecdhEncrypt( textBytes, - this.encryptionProps.keypair, + this.encryptionProps.diffieHellmanKeyPair, otherMember.publicKey.toBytes(), encryptionNonce, @@ -93,8 +93,8 @@ export type DialectAttributes = { }; export interface EncryptionProps { - keypair: Curve25519KeyPair; - publicKey: Ed25519Key; + diffieHellmanKeyPair: Curve25519KeyPair; + ed25519PublicKey: Ed25519Key; } export class TextSerdeFactory { From 7cdf0d2dcc834d3771baae8cbf9ca6eb5b8b7b1c Mon Sep 17 00:00:00 2001 From: cbosborn Date: Sun, 3 Apr 2022 23:29:15 -0400 Subject: [PATCH 05/10] WIP. Removed all instances of keypair in api/index.ts, replaced with Wallet. Not finished in text-serde, or tests and examples. --- src/api/index.ts | 65 +++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/src/api/index.ts b/src/api/index.ts index 2a90dcb..cbc0429 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,7 +1,7 @@ import * as anchor from '@project-serum/anchor'; import { EventParser } from '@project-serum/anchor'; import { Wallet } from '@project-serum/anchor/src/provider'; -import { Connection, Keypair, PublicKey } from '@solana/web3.js'; +import { Connection, PublicKey } from '@solana/web3.js'; import { sleep, waitForFinality, Wallet_ } from '../utils'; import { ENCRYPTION_OVERHEAD_BYTES } from '../utils/ecdh-encryption'; @@ -118,37 +118,37 @@ export async function getMetadataProgramAddress( // TODO: Simplify this function further now that we're no longer decrypting the device token. export async function getMetadata( program: anchor.Program, - user: PublicKey | anchor.web3.Keypair, - otherParty?: PublicKey | anchor.web3.Keypair | null, + user: PublicKey | Wallet, + otherParty?: PublicKey | Wallet | null, ): Promise { let shouldDecrypt = false; - let userIsKeypair = false; - let otherPartyIsKeypair = false; + let userIsWallet = false; + let otherPartyIsWallet = false; try { // assume user is pubkey new anchor.web3.PublicKey(user.toString()); } catch { - // user is keypair - userIsKeypair = true; + // user is wallet + userIsWallet = true; } try { // assume otherParty is pubkey new anchor.web3.PublicKey(otherParty?.toString() || ''); } catch { - // otherParty is keypair or null - otherPartyIsKeypair = (otherParty && true) || false; + // otherParty is wallet or null + otherPartyIsWallet = (otherParty && true) || false; } - if (otherParty && (userIsKeypair || otherPartyIsKeypair)) { + if (otherParty && (userIsWallet || otherPartyIsWallet)) { // cases 3 - 5 shouldDecrypt = true; } const [metadataAddress] = await getMetadataProgramAddress( program, - userIsKeypair ? (user as Keypair).publicKey : (user as PublicKey), + userIsWallet ? (user as Wallet).publicKey : (user as PublicKey), ); const metadata = await program.account.metadataAccount.fetch(metadataAddress); @@ -162,41 +162,41 @@ export async function getMetadata( export async function createMetadata( program: anchor.Program, - user: anchor.web3.Keypair | Wallet, ): Promise { + const wallet = program.provider.wallet; + const publicKey = wallet.publicKey; const [metadataAddress, metadataNonce] = await getMetadataProgramAddress( program, - user.publicKey, + publicKey, ); const tx = await program.rpc.createMetadata(new anchor.BN(metadataNonce), { accounts: { - user: user.publicKey, + user: publicKey, metadata: metadataAddress, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }, - signers: 'secretKey' in user ? [user] : [], }); await waitForFinality(program, tx); - return await getMetadata(program, user.publicKey); + return await getMetadata(program, publicKey); } export async function deleteMetadata( program: anchor.Program, - user: anchor.web3.Keypair | Wallet, ): Promise { + const wallet = program.provider.wallet; + const publicKey = wallet.publicKey; const [metadataAddress, metadataNonce] = await getMetadataProgramAddress( program, - user.publicKey, + publicKey, ); await program.rpc.closeMetadata(new anchor.BN(metadataNonce), { accounts: { - user: user.publicKey, + user: publicKey, metadata: metadataAddress, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }, - signers: 'secretKey' in user ? [user] : [], }); } @@ -204,9 +204,10 @@ export async function subscribeUser( program: anchor.Program, dialect: DialectAccount, user: PublicKey, - signer: Keypair, ): Promise { - const [publicKey, nonce] = await getDialectProgramAddress( + const wallet = program.provider.wallet; + const publicKey = wallet.publicKey; + const [dialectPublicKey, nonce] = await getDialectProgramAddress( program, dialect.dialect.members, ); @@ -219,14 +220,13 @@ export async function subscribeUser( new anchor.BN(metadataNonce), { accounts: { - dialect: publicKey, - signer: signer.publicKey, + dialect: dialectPublicKey, + signer: publicKey, user: user, metadata, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }, - signers: [signer], }, ); await waitForFinality(program, tx); @@ -320,7 +320,7 @@ export async function getDialect( export async function getDialects( program: anchor.Program, - user: anchor.web3.Keypair | Wallet, + user: Wallet, encryptionProps?: EncryptionProps, ): Promise { const metadata = await getMetadata(program, user.publicKey); @@ -394,11 +394,11 @@ export async function findDialects( export async function createDialect( program: anchor.Program, - owner: anchor.web3.Keypair | Wallet, members: Member[], encrypted = false, encryptionProps?: EncryptionProps, ): Promise { + const owner = program.provider.wallet; const sortedMembers = members.sort((a, b) => a.publicKey.toBuffer().compare(b.publicKey.toBuffer()), ); @@ -423,7 +423,6 @@ export async function createDialect( rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }, - signers: 'secretKey' in owner ? [owner] : [], }, ); await waitForFinality(program, tx); @@ -433,8 +432,8 @@ export async function createDialect( export async function deleteDialect( program: anchor.Program, { dialect }: DialectAccount, - owner: anchor.web3.Keypair | Wallet, ): Promise { + const wallet = program.provider.wallet; const [dialectPublicKey, nonce] = await getDialectProgramAddress( program, dialect.members, @@ -442,11 +441,10 @@ export async function deleteDialect( await program.rpc.closeDialect(new anchor.BN(nonce), { accounts: { dialect: dialectPublicKey, - owner: owner.publicKey, + owner: wallet.publicKey, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }, - signers: 'secretKey' in owner ? [owner] : [], }); } @@ -466,10 +464,10 @@ Messages export async function sendMessage( program: anchor.Program, { dialect, publicKey }: DialectAccount, - sender: anchor.web3.Keypair | Wallet, text: string, encryptionProps?: EncryptionProps, ): Promise { + const wallet = program.provider.wallet; const [dialectPublicKey, nonce] = await getDialectProgramAddress( program, dialect.members, @@ -488,13 +486,12 @@ export async function sendMessage( { accounts: { dialect: dialectPublicKey, - sender: sender ? sender.publicKey : program.provider.wallet.publicKey, + sender: wallet.publicKey, member0: dialect.members[0].publicKey, member1: dialect.members[1].publicKey, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }, - signers: sender && 'secretKey' in sender ? [sender] : [], }, ); const d = await getDialect(program, publicKey, encryptionProps); From 5ba00cc4c6949caaf80055dd4c2acd9666d0f622 Mon Sep 17 00:00:00 2001 From: cbosborn Date: Mon, 4 Apr 2022 00:05:15 -0400 Subject: [PATCH 06/10] Create metadata tests passing with new program arguments. --- tests/test-v1.ts | 46 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/tests/test-v1.ts b/tests/test-v1.ts index 8a782e7..717fdb3 100644 --- a/tests/test-v1.ts +++ b/tests/test-v1.ts @@ -1,5 +1,5 @@ import * as anchor from '@project-serum/anchor'; -import { AnchorError, Program } from '@project-serum/anchor'; +import { AnchorError, Idl, Program, Provider } from '@project-serum/anchor'; import * as web3 from '@solana/web3.js'; import chai, { expect } from 'chai'; import chaiAsPromised from 'chai-as-promised'; @@ -30,6 +30,7 @@ import { import { NONCE_SIZE_BYTES } from '../src/utils/nonce-generator'; import { randomInt } from 'crypto'; import { CountDownLatch } from '../src/utils/countdown-latch'; +import { idl, programs, Wallet_ } from '../src/utils'; import { EncryptionProps } from '../src/api/text-serde'; chai.use(chaiAsPromised); @@ -38,10 +39,11 @@ anchor.setProvider(anchor.Provider.local()); describe('Protocol v1 test', () => { const program: anchor.Program = anchor.workspace.Dialect; const connection = program.provider.connection; + console.log('program', program); describe('Metadata tests', () => { - let owner: web3.Keypair; - let writer: web3.Keypair; + let owner: Program; + let writer: Program; beforeEach(async () => { owner = ( @@ -49,19 +51,19 @@ describe('Protocol v1 test', () => { requestAirdrop: true, createMeta: false, }) - ).user; + ).program; writer = ( await createUser({ requestAirdrop: true, createMeta: false, }) - ).user; + ).program; }); it('Create user metadata object(s)', async () => { for (const member of [owner, writer]) { - const metadata = await createMetadata(program, member); - const gottenMetadata = await getMetadata(program, member.publicKey); + const metadata = await createMetadata(member); + const gottenMetadata = await getMetadata(member, member.provider.wallet.publicKey); expect(metadata).to.be.deep.eq(gottenMetadata); } }); @@ -1080,25 +1082,41 @@ describe('Protocol v1 test', () => { createMeta: true, }, ) { - const user = web3.Keypair.generate(); + const keypair = web3.Keypair.generate(); + const wallet = Wallet_.embedded(keypair.secretKey); + const RPC_URL = process.env.RPC_URL || 'http://localhost:8899'; + const dialectConnection = new web3.Connection(RPC_URL, 'recent'); + const dialectProvider = new Provider( + dialectConnection, + wallet, + Provider.defaultOptions(), + ); + // @ts-ignore + const NETWORK_NAME = 'localnet'; + const DIALECT_PROGRAM_ADDRESS = programs[NETWORK_NAME].programAddress; + const program = new Program( + idl as Idl, + new web3.PublicKey(DIALECT_PROGRAM_ADDRESS), + dialectProvider, + ); if (requestAirdrop) { const airDropRequest = await connection.requestAirdrop( - user.publicKey, + wallet.publicKey, 10 * web3.LAMPORTS_PER_SOL, ); await connection.confirmTransaction(airDropRequest); } if (createMeta) { - await createMetadata(program, user); + await createMetadata(program); } const encryptionProps = { - ed25519PublicKey: user.publicKey.toBytes(), + ed25519PublicKey: keypair.publicKey.toBytes(), diffieHellmanKeyPair: ed25519KeyPairToCurve25519({ - publicKey: user.publicKey.toBytes(), - secretKey: user.secretKey, + publicKey: keypair.publicKey.toBytes(), + secretKey: keypair.secretKey, }), }; - return { user, encryptionProps }; + return { program, encryptionProps }; } }); From b790d23f3adce18e2266775e79a359fed26465d5 Mon Sep 17 00:00:00 2001 From: cbosborn Date: Mon, 4 Apr 2022 00:08:38 -0400 Subject: [PATCH 07/10] Fix delete metadata tests. --- tests/test-v1.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/test-v1.ts b/tests/test-v1.ts index 717fdb3..05c46d6 100644 --- a/tests/test-v1.ts +++ b/tests/test-v1.ts @@ -39,7 +39,6 @@ anchor.setProvider(anchor.Provider.local()); describe('Protocol v1 test', () => { const program: anchor.Program = anchor.workspace.Dialect; const connection = program.provider.connection; - console.log('program', program); describe('Metadata tests', () => { let owner: Program; @@ -70,11 +69,11 @@ describe('Protocol v1 test', () => { it('Owner deletes metadata', async () => { for (const member of [owner, writer]) { - await createMetadata(program, member); - await getMetadata(program, member.publicKey); - await deleteMetadata(program, member); + await createMetadata(member); + await getMetadata(member, member.provider.wallet.publicKey); + await deleteMetadata(member); chai - .expect(getMetadata(program, member.publicKey)) + .expect(getMetadata(member, member.provider.wallet.publicKey)) .to.eventually.be.rejectedWith(Error); } }); From 52de37bcda160b79b96e0d813ab013cf85b2c25d Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Thu, 7 Apr 2022 18:49:16 +0400 Subject: [PATCH 08/10] Add user type to simplify tests --- tests/test-v1.ts | 2063 +++++++++++++++++++++++----------------------- 1 file changed, 1028 insertions(+), 1035 deletions(-) diff --git a/tests/test-v1.ts b/tests/test-v1.ts index 05c46d6..428291d 100644 --- a/tests/test-v1.ts +++ b/tests/test-v1.ts @@ -1,36 +1,20 @@ import * as anchor from '@project-serum/anchor'; -import { AnchorError, Idl, Program, Provider } from '@project-serum/anchor'; +import { Idl, Program, Provider } from '@project-serum/anchor'; import * as web3 from '@solana/web3.js'; +import { PublicKey } from '@solana/web3.js'; import chai, { expect } from 'chai'; import chaiAsPromised from 'chai-as-promised'; import { createDialect, createMetadata, - deleteDialect, deleteMetadata, - DialectAccount, - Event, - findDialects, - getDialect, - getDialectForMembers, - getDialectProgramAddress, - getDialects, getMetadata, Member, - sendMessage, - subscribeToEvents, subscribeUser, } from '../src/api'; -import { sleep } from '../src/utils'; -import { ITEM_METADATA_OVERHEAD } from '../src/utils/cyclic-bytebuffer'; -import { - ed25519KeyPairToCurve25519, - ENCRYPTION_OVERHEAD_BYTES, -} from '../src/utils/ecdh-encryption'; -import { NONCE_SIZE_BYTES } from '../src/utils/nonce-generator'; -import { randomInt } from 'crypto'; -import { CountDownLatch } from '../src/utils/countdown-latch'; import { idl, programs, Wallet_ } from '../src/utils'; +import { ed25519KeyPairToCurve25519 } from '../src/utils/ecdh-encryption'; +import { Wallet } from '../src/utils/Wallet'; import { EncryptionProps } from '../src/api/text-serde'; chai.use(chaiAsPromised); @@ -41,1046 +25,1054 @@ describe('Protocol v1 test', () => { const connection = program.provider.connection; describe('Metadata tests', () => { - let owner: Program; - let writer: Program; - - beforeEach(async () => { - owner = ( - await createUser({ - requestAirdrop: true, - createMeta: false, - }) - ).program; - writer = ( - await createUser({ - requestAirdrop: true, - createMeta: false, - }) - ).program; - }); - - it('Create user metadata object(s)', async () => { - for (const member of [owner, writer]) { - const metadata = await createMetadata(member); - const gottenMetadata = await getMetadata(member, member.provider.wallet.publicKey); - expect(metadata).to.be.deep.eq(gottenMetadata); - } - }); - - it('Owner deletes metadata', async () => { - for (const member of [owner, writer]) { - await createMetadata(member); - await getMetadata(member, member.provider.wallet.publicKey); - await deleteMetadata(member); - chai - .expect(getMetadata(member, member.provider.wallet.publicKey)) - .to.eventually.be.rejectedWith(Error); - } - }); - }); - - describe('Dialect initialization tests', () => { - let owner: web3.Keypair; - let writer: web3.Keypair; - let nonmember: web3.Keypair; - - let members: Member[] = []; - - beforeEach(async () => { - owner = ( - await createUser({ - requestAirdrop: true, - createMeta: true, - }) - ).user; - writer = ( - await createUser({ - requestAirdrop: true, - createMeta: true, - }) - ).user; - nonmember = ( - await createUser({ - requestAirdrop: true, - createMeta: false, - }) - ).user; - members = [ - { - publicKey: owner.publicKey, - scopes: [true, false], // owner, read-only - }, - { - publicKey: writer.publicKey, - scopes: [false, true], // non-owner, read-write - }, - ]; - }); - - it('Confirm only each user (& dialect) can read encrypted device tokens', async () => { - // TODO: Implement - chai.expect(true).to.be.true; - }); - - it("Fail to create a dialect if the owner isn't a member with admin privileges", async () => { - try { - await createDialect(program, nonmember, members, true); - chai.assert( - false, - "Creating a dialect whose owner isn't a member should fail.", - ); - } catch (e) { - chai.assert( - (e as AnchorError).message.includes( - 'The dialect owner must be a member with admin privileges.', - ), - ); - } - - try { - // TODO: write this in a nicer way - await createDialect(program, writer, members, true); - chai.assert( - false, - "Creating a dialect whose owner isn't a member should fail.", - ); - } catch (e) { - chai.assert( - (e as AnchorError).message.includes( - 'The dialect owner must be a member with admin privileges.', - ), - ); - } - }); - - it('Fail to create a dialect for unsorted members', async () => { - // use custom unsorted version of createDialect for unsorted members - const unsortedMembers = members.sort( - (a, b) => -a.publicKey.toBuffer().compare(b.publicKey.toBuffer()), - ); - const [publicKey, nonce] = await getDialectProgramAddress( - program, - unsortedMembers, - ); - // TODO: assert owner in members - const keyedMembers = unsortedMembers.reduce( - (ms, m, idx) => ({ ...ms, [`member${idx}`]: m.publicKey }), - {}, - ); - chai - .expect( - program.rpc.createDialect( - new anchor.BN(nonce), - members.map((m) => m.scopes), - { - accounts: { - dialect: publicKey, - owner: owner.publicKey, - ...keyedMembers, - rent: anchor.web3.SYSVAR_RENT_PUBKEY, - systemProgram: anchor.web3.SystemProgram.programId, - }, - signers: [owner], - }, - ), - ) - .to.eventually.be.rejectedWith(Error); - }); - - it('Create encrypted dialect for 2 members, with owner and write scopes, respectively', async () => { - const dialectAccount = await createDialect(program, owner, members, true); - expect(dialectAccount.dialect.encrypted).to.be.true; - }); - - it('Create unencrypted dialect for 2 members, with owner and write scopes, respectively', async () => { - const dialectAccount = await createDialect( - program, - owner, - members, - false, - ); - expect(dialectAccount.dialect.encrypted).to.be.false; - }); - - it('Creates unencrypted dialect by default', async () => { - const dialectAccount = await createDialect(program, owner, members); - expect(dialectAccount.dialect.encrypted).to.be.false; - }); - - it('Fail to create a second dialect for the same members', async () => { - chai - .expect(createDialect(program, owner, members)) - .to.eventually.be.rejectedWith(Error); - }); - - it('Fail to create a dialect for duplicate members', async () => { - const duplicateMembers = [ - { publicKey: owner.publicKey, scopes: [true, true] } as Member, - { publicKey: owner.publicKey, scopes: [true, true] } as Member, - ]; - chai - .expect(createDialect(program, owner, duplicateMembers)) - .to.be.rejectedWith(Error); - }); - - it('Find a dialect for a given member pair, verify correct scopes.', async () => { - await createDialect(program, owner, members); - const dialect = await getDialectForMembers(program, members); - members.every((m, i) => - expect( - m.publicKey.equals(dialect.dialect.members[i].publicKey) && - m.scopes.every( - (s, j) => s === dialect.dialect.members[i].scopes[j], - ), - ), - ); - }); - - it('Subscribe users to dialect', async () => { - const dialect = await createDialect(program, owner, members); - // owner subscribes themselves - await subscribeUser(program, dialect, owner.publicKey, owner); - // owner subscribes writer - await subscribeUser(program, dialect, writer.publicKey, owner); - const ownerMeta = await getMetadata(program, owner.publicKey); - const writerMeta = await getMetadata(program, writer.publicKey); - chai - .expect( - ownerMeta.subscriptions.filter((s) => - s.pubkey.equals(dialect.publicKey), - ).length, - ) - .to.equal(1); - chai - .expect( - writerMeta.subscriptions.filter((s) => - s.pubkey.equals(dialect.publicKey), - ).length, - ) - .to.equal(1); - }); - - it('Should return list of dialects sorted by time desc', async () => { - // given - console.log('Creating users'); - const [user1, user2, user3] = await Promise.all([ - createUser({ - requestAirdrop: true, - createMeta: true, - }).then((it) => it.user), - createUser({ - requestAirdrop: true, - createMeta: true, - }).then((it) => it.user), - createUser({ - requestAirdrop: true, - createMeta: true, - }).then((it) => it.user), - ]); - console.log('Creating dialects'); - // create first dialect and subscribe users - const dialect1 = await createDialectAndSubscribeAllMembers( - program, - user1, - user2, - false, - ); - const dialect2 = await createDialectAndSubscribeAllMembers( - program, - user1, - user3, - false, - ); - // when - const afterCreatingDialects = await getDialects(program, user1); - await sleep(3000); // wait a bit to avoid equal timestamp, since since we get utc seconds as a timestamp - await sendMessage( - program, - dialect1, - user1, - 'Dummy message to increment latest message timestamp', - ); - const afterSendingMessageToDialect1 = await getDialects(program, user1); - await sleep(3000); // wait a bit to avoid equal timestamp, since since we get utc seconds as a timestamp - await sendMessage( - program, - dialect2, - user1, - 'Dummy message to increment latest message timestamp', - ); - const afterSendingMessageToDialect2 = await getDialects(program, user1); - // then - // assert dialects before sending messages - chai - .expect(afterCreatingDialects.map((it) => it.publicKey)) - .to.be.deep.eq([dialect2.publicKey, dialect1.publicKey]); // dialect 2 was created after dialect 1 - // assert dialects after sending message to first dialect - chai - .expect(afterSendingMessageToDialect1.map((it) => it.publicKey)) - .to.be.deep.eq([dialect1.publicKey, dialect2.publicKey]); - // assert dialects after sending message to second dialect - chai - .expect(afterSendingMessageToDialect2.map((it) => it.publicKey)) - .to.be.deep.eq([dialect2.publicKey, dialect1.publicKey]); - }); - - it('Non-owners fail to delete the dialect', async () => { - const dialect = await createDialect(program, owner, members); - chai - .expect(deleteDialect(program, dialect, writer)) - .to.eventually.be.rejectedWith(Error); - chai - .expect(deleteDialect(program, dialect, nonmember)) - .to.eventually.be.rejectedWith(Error); - }); - - it('Owner deletes the dialect', async () => { - const dialect = await createDialect(program, owner, members); - await deleteDialect(program, dialect, owner); - chai - .expect(getDialectForMembers(program, members)) - .to.eventually.be.rejectedWith(Error); - }); - - it('Fail to subscribe a user twice to the same dialect (silent, noop)', async () => { - const dialect = await createDialect(program, owner, members); - await subscribeUser(program, dialect, writer.publicKey, owner); - const metadata = await getMetadata(program, writer.publicKey); - // subscribed once - chai - .expect( - metadata.subscriptions.filter((s) => - s.pubkey.equals(dialect.publicKey), - ).length, - ) - .to.equal(1); - chai - .expect(subscribeUser(program, dialect, writer.publicKey, owner)) - .to.be.rejectedWith(Error); - // still subscribed just once - chai - .expect( - metadata.subscriptions.filter((s) => - s.pubkey.equals(dialect.publicKey), - ).length, - ) - .to.equal(1); - }); - }); - - describe('Find dialects', () => { - it('Can find all dialects filtering by user public key', async () => { - // given - const [user1, user2, user3] = await Promise.all([ - createUser({ - requestAirdrop: true, - createMeta: false, - }).then((it) => it.user), - createUser({ - requestAirdrop: true, - createMeta: false, - }).then((it) => it.user), - createUser({ - requestAirdrop: true, - createMeta: false, - }).then((it) => it.user), - ]); - const [user1User2Dialect, user1User3Dialect, user2User3Dialect] = - await Promise.all([ - createDialect(program, user1, [ - { - publicKey: user1.publicKey, - scopes: [true, true], - }, - { - publicKey: user2.publicKey, - scopes: [false, true], - }, - ]), - createDialect(program, user1, [ - { - publicKey: user1.publicKey, - scopes: [true, true], - }, - { - publicKey: user3.publicKey, - scopes: [false, true], - }, - ]), - createDialect(program, user2, [ - { - publicKey: user2.publicKey, - scopes: [true, true], - }, - { - publicKey: user3.publicKey, - scopes: [false, true], - }, - ]), - ]); - // when - const [ - user1Dialects, - user2Dialects, - user3Dialects, - nonExistingUserDialects, - ] = await Promise.all([ - findDialects(program, { - userPk: user1.publicKey, - }), - findDialects(program, { - userPk: user2.publicKey, - }), - findDialects(program, { - userPk: user3.publicKey, - }), - findDialects(program, { - userPk: anchor.web3.Keypair.generate().publicKey, - }), - ]); - // then - expect( - user1Dialects.map((it) => it.publicKey), - ).to.deep.contain.all.members([ - user1User2Dialect.publicKey, - user1User3Dialect.publicKey, - ]); - expect( - user2Dialects.map((it) => it.publicKey), - ).to.deep.contain.all.members([ - user1User2Dialect.publicKey, - user2User3Dialect.publicKey, - ]); - expect( - user3Dialects.map((it) => it.publicKey), - ).to.deep.contain.all.members([ - user2User3Dialect.publicKey, - user1User3Dialect.publicKey, - ]); - expect(nonExistingUserDialects.length).to.be.eq(0); - }); - }); - - describe('Unencrypted messaging tests', () => { - let owner: web3.Keypair; - let writer: web3.Keypair; - let nonmember: web3.Keypair; - let members: Member[] = []; - let dialect: DialectAccount; + let owner: User; + let writer: User; beforeEach(async () => { - (owner = await createUser({ - requestAirdrop: true, - createMeta: true, - }).then((it) => it.user)), - (writer = await createUser({ - requestAirdrop: true, - createMeta: true, - }).then((it) => it.user)), - (nonmember = await createUser({ - requestAirdrop: true, - createMeta: false, - }).then((it) => it.user)), - (members = [ - { - publicKey: owner.publicKey, - scopes: [true, false], // owner, read-only - }, - { - publicKey: writer.publicKey, - scopes: [false, true], // non-owner, read-write - }, - ]); - dialect = await createDialect(program, owner, members, false); - }); - - it('Message sender and receiver can read the message text and time', async () => { - // given - const dialect = await getDialectForMembers(program, members); - const text = generateRandomText(256); - // when - await sendMessage(program, dialect, writer, text); - // then - const senderDialect = await getDialectForMembers( - program, - dialect.dialect.members, - ); - const message = senderDialect.dialect.messages[0]; - chai.expect(message.text).to.be.eq(text); - chai - .expect(senderDialect.dialect.lastMessageTimestamp) - .to.be.eq(message.timestamp); - }); - - it('Anonymous user can read any of the messages', async () => { - // given - const senderDialect = await getDialectForMembers(program, members); - const text = generateRandomText(256); - await sendMessage(program, senderDialect, writer, text); - // when / then - const nonMemberDialect = await getDialectForMembers( - program, - dialect.dialect.members, - ); - const message = nonMemberDialect.dialect.messages[0]; - chai.expect(message.text).to.be.eq(text); - chai.expect(message.owner).to.be.deep.eq(writer.publicKey); - chai - .expect(nonMemberDialect.dialect.lastMessageTimestamp) - .to.be.eq(message.timestamp); - }); - - it('New messages overwrite old, retrieved messages are in order.', async () => { - // emulate ideal message alignment withing buffer - const rawBufferSize = 8192; - const messagesPerDialect = 16; - const numMessages = messagesPerDialect * 2; - const salt = 3; - const targetRawMessageSize = rawBufferSize / messagesPerDialect - salt; - const timestampSize = 4; - const ownerMemberIdxSize = 1; - const messageSerializationOverhead = - ITEM_METADATA_OVERHEAD + timestampSize + ownerMemberIdxSize; - const targetTextSize = - targetRawMessageSize - messageSerializationOverhead; - const texts = Array(numMessages) - .fill(0) - .map(() => generateRandomText(targetTextSize)); - for (let messageIdx = 0; messageIdx < numMessages; messageIdx++) { - // verify last last N messages look correct - const messageCounter = messageIdx + 1; - const text = texts[messageIdx]; - const dialect = await getDialectForMembers(program, members); - console.log( - `Sending message ${messageCounter}/${texts.length} - len = ${text.length} - idx: ${dialect.dialect.nextMessageIdx}`, - ); - await sendMessage(program, dialect, writer, text); - const sliceStart = - messageCounter <= messagesPerDialect - ? 0 - : messageCounter - messagesPerDialect; - const expectedMessagesCount = Math.min( - messageCounter, - messagesPerDialect, - ); - const sliceEnd = sliceStart + expectedMessagesCount; - const expectedMessages = texts.slice(sliceStart, sliceEnd).reverse(); - const d = await getDialect(program, dialect.publicKey); - const actualMessages = d.dialect.messages.map((m) => m.text); - console.log(` msgs count after send: ${actualMessages.length}\n`); - expect(actualMessages).to.be.deep.eq(expectedMessages); - } - }); - - it('Message text limit of 853 bytes can be sent/received', async () => { - const maxMessageSizeBytes = 853; - const texts = Array(30) - .fill(0) - .map(() => generateRandomText(maxMessageSizeBytes)); - for (let messageIdx = 0; messageIdx < texts.length; messageIdx++) { - const text = texts[messageIdx]; - const messageCounter = messageIdx + 1; - const dialect = await getDialectForMembers(program, members); - console.log( - `Sending message ${messageCounter}/${texts.length} - len = ${text.length} - idx: ${dialect.dialect.nextMessageIdx}`, - ); - // when - await sendMessage(program, dialect, writer, text); - const d = await getDialect(program, dialect.publicKey); - const actualMessages = d.dialect.messages; - const lastMessage = actualMessages[0]; - console.log(` msgs count after send: ${actualMessages.length}\n`); - // then - expect(lastMessage.text).to.be.deep.eq(text); - } - }); - }); - - describe('Encrypted messaging tests', () => { - let owner: web3.Keypair; - let ownerEncryptionProps: EncryptionProps; - let writer: web3.Keypair; - let writerEncryptionProps: EncryptionProps; - let nonmember: web3.Keypair; - let nonmemberEncryptionProps: EncryptionProps; - let members: Member[] = []; - let dialect: DialectAccount; - - beforeEach(async () => { - const ownerUser = await createUser({ - requestAirdrop: true, - createMeta: true, - }); - owner = ownerUser.user; - ownerEncryptionProps = ownerUser.encryptionProps; - const writerUser = await createUser({ + owner = await createUser({ requestAirdrop: true, - createMeta: true, + createMeta: false, }); - writer = writerUser.user; - writerEncryptionProps = writerUser.encryptionProps; - const nonmemberUser = await createUser({ + writer = await createUser({ requestAirdrop: true, createMeta: false, }); - nonmember = nonmemberUser.user; - nonmemberEncryptionProps = nonmemberUser.encryptionProps; - members = [ - { - publicKey: owner.publicKey, - scopes: [true, false], // owner, read-only - }, - { - publicKey: writer.publicKey, - scopes: [false, true], // non-owner, read-write - }, - ]; - dialect = await createDialect(program, owner, members, true); - }); - - it('Message sender can send msg and then read the message text and time', async () => { - // given - const dialect = await getDialectForMembers( - program, - members, - writerEncryptionProps, - ); - const text = generateRandomText(256); - // when - await sendMessage(program, dialect, writer, text, writerEncryptionProps); - // then - const senderDialect = await getDialectForMembers( - program, - dialect.dialect.members, - writerEncryptionProps, - ); - const message = senderDialect.dialect.messages[0]; - chai.expect(message.text).to.be.eq(text); - chai.expect(message.owner).to.be.deep.eq(writer.publicKey); - chai - .expect(senderDialect.dialect.lastMessageTimestamp) - .to.be.eq(message.timestamp); - }); - - it('Message receiver can read the message text and time sent by sender', async () => { - // given - const senderDialect = await getDialectForMembers( - program, - members, - writerEncryptionProps, - ); - const text = generateRandomText(256); - // when - await sendMessage( - program, - senderDialect, - writer, - text, - writerEncryptionProps, - ); - // then - const receiverDialect = await getDialectForMembers( - program, - dialect.dialect.members, - ownerEncryptionProps, - ); - const message = receiverDialect.dialect.messages[0]; - chai.expect(message.text).to.be.eq(text); - chai.expect(message.owner).to.be.deep.eq(writer.publicKey); - chai - .expect(receiverDialect.dialect.lastMessageTimestamp) - .to.be.eq(message.timestamp); - }); - - it("Non-member can't read (decrypt) any of the messages", async () => { - // given - const senderDialect = await getDialectForMembers( - program, - members, - writerEncryptionProps, - ); - const text = generateRandomText(256); - await sendMessage( - program, - senderDialect, - writer, - text, - writerEncryptionProps, - ); - // when / then - expect( - getDialectForMembers( - program, - dialect.dialect.members, - nonmemberEncryptionProps, - ), - ).to.eventually.be.rejected; }); - it('New messages overwrite old, retrieved messages are in order.', async () => { - // emulate ideal message alignment withing buffer - const rawBufferSize = 8192; - const messagesPerDialect = 16; - const numMessages = messagesPerDialect * 2; - const salt = 3; - const targetRawMessageSize = rawBufferSize / messagesPerDialect - salt; - const timestampSize = 4; - const ownerMemberIdxSize = 1; - const messageSerializationOverhead = - ITEM_METADATA_OVERHEAD + - ENCRYPTION_OVERHEAD_BYTES + - NONCE_SIZE_BYTES + - timestampSize + - ownerMemberIdxSize; - const targetTextSize = - targetRawMessageSize - messageSerializationOverhead; - const texts = Array(numMessages) - .fill(0) - .map(() => generateRandomText(targetTextSize)); - for (let messageIdx = 0; messageIdx < numMessages; messageIdx++) { - // verify last last N messages look correct - const messageCounter = messageIdx + 1; - const text = texts[messageIdx]; - const dialect = await getDialectForMembers( - program, - members, - writerEncryptionProps, - ); - console.log( - `Sending message ${messageCounter}/${texts.length} - len = ${text.length} - idx: ${dialect.dialect.nextMessageIdx}`, - ); - await sendMessage( - program, - dialect, - writer, - text, - writerEncryptionProps, - ); - const sliceStart = - messageCounter <= messagesPerDialect - ? 0 - : messageCounter - messagesPerDialect; - const expectedMessagesCount = Math.min( - messageCounter, - messagesPerDialect, - ); - const sliceEnd = sliceStart + expectedMessagesCount; - const expectedMessages = texts.slice(sliceStart, sliceEnd).reverse(); - const d = await getDialect( - program, - dialect.publicKey, - writerEncryptionProps, - ); - const actualMessages = d.dialect.messages.map((m) => m.text); - console.log(` msgs count after send: ${actualMessages.length}\n`); - expect(actualMessages).to.be.deep.eq(expectedMessages); - } - }); - - it('Send/receive random size messages.', async () => { - const texts = Array(32) - .fill(0) - .map(() => generateRandomText(randomInt(256, 512))); - for (let messageIdx = 0; messageIdx < texts.length; messageIdx++) { - const text = texts[messageIdx]; - const messageCounter = messageIdx + 1; - const dialect = await getDialectForMembers( - program, - members, - writerEncryptionProps, - ); - console.log( - `Sending message ${messageCounter}/${texts.length} - len = ${text.length} - idx: ${dialect.dialect.nextMessageIdx}`, - ); - // when - await sendMessage( - program, - dialect, - writer, - text, - writerEncryptionProps, - ); - const d = await getDialect( + it('Create user metadata object(s)', async () => { + for (const user of [owner, writer]) { + const { program, publicKey } = user; + const metadata = await createMetadata(program); + const gottenMetadata = await getMetadata( program, - dialect.publicKey, - writerEncryptionProps, + publicKey, // TODO: what for this is needed? ); - const actualMessages = d.dialect.messages; - const lastMessage = actualMessages[0]; - console.log(` msgs count after send: ${actualMessages.length}\n`); - // then - expect(lastMessage.text).to.be.deep.eq(text); + expect(metadata).to.be.deep.eq(gottenMetadata); } }); - /* UTF-8 encoding summary: - - ASCII characters are encoded using 1 byte - - Roman, Greek, Cyrillic, Coptic, Armenian, Hebrew, Arabic characters are encoded using 2 bytes - - Chinese and Japanese among others are encoded using 3 bytes - - Emoji are encoded using 4 bytes - A note about message length limit and summary: - - len >= 814 hits max transaction size limit = 1232 bytes https://docs.solana.com/ru/proposals/transactions-v2 - - => best case: 813 symbols per msg (ascii only) - - => worst case: 203 symbols (e.g. emoji only) - - => average case depends on character set, see details below: - ---- ASCII: ±800 characters - ---- Roman, Greek, Cyrillic, Coptic, Armenian, Hebrew, Arabic: ± 406 characters - ---- Chinese and japanese: ± 270 characters - ---- Emoji: ± 203 characters*/ - it('Message text limit of 813 bytes can be sent/received', async () => { - const maxMessageSizeBytes = 813; - const texts = Array(30) - .fill(0) - .map(() => generateRandomText(maxMessageSizeBytes)); - for (let messageIdx = 0; messageIdx < texts.length; messageIdx++) { - const text = texts[messageIdx]; - const messageCounter = messageIdx + 1; - const dialect = await getDialectForMembers( - program, - members, - writerEncryptionProps, - ); - console.log( - `Sending message ${messageCounter}/${texts.length} - len = ${text.length} - idx: ${dialect.dialect.nextMessageIdx}`, - ); - // when - await sendMessage( - program, - dialect, - writer, - text, - writerEncryptionProps, - ); - const d = await getDialect( - program, - dialect.publicKey, - writerEncryptionProps, - ); - const actualMessages = d.dialect.messages; - const lastMessage = actualMessages[0]; - console.log(` msgs count after send: ${actualMessages.length}\n`); - // then - expect(lastMessage.text).to.be.deep.eq(text); + it('Owner deletes metadata', async () => { + for (const user of [owner, writer]) { + const { program, publicKey } = user; + await createMetadata(program); + await getMetadata(program, publicKey); + await deleteMetadata(program); + chai + .expect(getMetadata(program, publicKey)) + .to.eventually.be.rejectedWith(Error); } }); + }); - it('2 writers can send a messages and read them when dialect state is linearized before sending msg', async () => { - // given - const writer1 = await createUser({ - requestAirdrop: true, - createMeta: true, - }); - const writer2 = await createUser({ - requestAirdrop: true, - createMeta: true, - }); - members = [ - { - publicKey: writer1.user.publicKey, - scopes: [true, true], // owner, read-only - }, - { - publicKey: writer2.user.publicKey, - scopes: [false, true], // non-owner, read-write - }, - ]; - await createDialect(program, writer1.user, members, true); - // when - let writer1Dialect = await getDialectForMembers( - program, - members, - writer1.encryptionProps, - ); - const writer1Text = generateRandomText(256); - await sendMessage( - program, - writer1Dialect, - writer1.user, - writer1Text, - writer1.encryptionProps, - ); - let writer2Dialect = await getDialectForMembers( - program, - members, - writer2.encryptionProps, - ); // ensures dialect state linearization - const writer2Text = generateRandomText(256); - await sendMessage( - program, - writer2Dialect, - writer2.user, - writer2Text, - writer2.encryptionProps, - ); + // describe('Dialect initialization tests', () => { + // let owner: Program; + // let writer: Program; + // let nonmember: Program; + // + // let members: Member[] = []; + // + // beforeEach(async () => { + // owner = ( + // await createUser({ + // requestAirdrop: true, + // createMeta: true, + // }) + // ).program; + // writer = ( + // await createUser({ + // requestAirdrop: true, + // createMeta: true, + // }) + // ).program; + // nonmember = ( + // await createUser({ + // requestAirdrop: true, + // createMeta: false, + // }) + // ).program; + // members = [ + // { + // publicKey: owner.provider.wallet.publicKey, + // scopes: [true, false], // owner, read-only + // }, + // { + // publicKey: writer.publicKey, + // scopes: [false, true], // non-owner, read-write + // }, + // ]; + // }); + // + // it('Confirm only each user (& dialect) can read encrypted device tokens', async () => { + // // TODO: Implement + // chai.expect(true).to.be.true; + // }); + // + // it("Fail to create a dialect if the owner isn't a member with admin privileges", async () => { + // try { + // await createDialect(program, nonmember, members, true); + // chai.assert( + // false, + // "Creating a dialect whose owner isn't a member should fail.", + // ); + // } catch (e) { + // chai.assert( + // (e as AnchorError).message.includes( + // 'The dialect owner must be a member with admin privileges.', + // ), + // ); + // } + // + // try { + // // TODO: write this in a nicer way + // await createDialect(program, writer, members, true); + // chai.assert( + // false, + // "Creating a dialect whose owner isn't a member should fail.", + // ); + // } catch (e) { + // chai.assert( + // (e as AnchorError).message.includes( + // 'The dialect owner must be a member with admin privileges.', + // ), + // ); + // } + // }); + // + // it('Fail to create a dialect for unsorted members', async () => { + // // use custom unsorted version of createDialect for unsorted members + // const unsortedMembers = members.sort( + // (a, b) => -a.publicKey.toBuffer().compare(b.publicKey.toBuffer()), + // ); + // const [publicKey, nonce] = await getDialectProgramAddress( + // program, + // unsortedMembers, + // ); + // // TODO: assert owner in members + // const keyedMembers = unsortedMembers.reduce( + // (ms, m, idx) => ({ ...ms, [`member${idx}`]: m.publicKey }), + // {}, + // ); + // chai + // .expect( + // program.rpc.createDialect( + // new anchor.BN(nonce), + // members.map((m) => m.scopes), + // { + // accounts: { + // dialect: publicKey, + // owner: owner.publicKey, + // ...keyedMembers, + // rent: anchor.web3.SYSVAR_RENT_PUBKEY, + // systemProgram: anchor.web3.SystemProgram.programId, + // }, + // signers: [owner], + // }, + // ), + // ) + // .to.eventually.be.rejectedWith(Error); + // }); + // + // it('Create encrypted dialect for 2 members, with owner and write scopes, respectively', async () => { + // const dialectAccount = await createDialect(program, owner, members, true); + // expect(dialectAccount.dialect.encrypted).to.be.true; + // }); + // + // it('Create unencrypted dialect for 2 members, with owner and write scopes, respectively', async () => { + // const dialectAccount = await createDialect( + // program, + // owner, + // members, + // false, + // ); + // expect(dialectAccount.dialect.encrypted).to.be.false; + // }); + // + // it('Creates unencrypted dialect by default', async () => { + // const dialectAccount = await createDialect(program, owner, members); + // expect(dialectAccount.dialect.encrypted).to.be.false; + // }); + // + // it('Fail to create a second dialect for the same members', async () => { + // chai + // .expect(createDialect(program, owner, members)) + // .to.eventually.be.rejectedWith(Error); + // }); + // + // it('Fail to create a dialect for duplicate members', async () => { + // const duplicateMembers = [ + // { publicKey: owner.publicKey, scopes: [true, true] } as Member, + // { publicKey: owner.publicKey, scopes: [true, true] } as Member, + // ]; + // chai + // .expect(createDialect(program, owner, duplicateMembers)) + // .to.be.rejectedWith(Error); + // }); + // + // it('Find a dialect for a given member pair, verify correct scopes.', async () => { + // await createDialect(program, owner, members); + // const dialect = await getDialectForMembers(program, members); + // members.every((m, i) => + // expect( + // m.publicKey.equals(dialect.dialect.members[i].publicKey) && + // m.scopes.every( + // (s, j) => s === dialect.dialect.members[i].scopes[j], + // ), + // ), + // ); + // }); + // + // it('Subscribe users to dialect', async () => { + // const dialect = await createDialect(program, owner, members); + // // owner subscribes themselves + // await subscribeUser(program, dialect, owner.publicKey, owner); + // // owner subscribes writer + // await subscribeUser(program, dialect, writer.publicKey, owner); + // const ownerMeta = await getMetadata(program, owner.publicKey); + // const writerMeta = await getMetadata(program, writer.publicKey); + // chai + // .expect( + // ownerMeta.subscriptions.filter((s) => + // s.pubkey.equals(dialect.publicKey), + // ).length, + // ) + // .to.equal(1); + // chai + // .expect( + // writerMeta.subscriptions.filter((s) => + // s.pubkey.equals(dialect.publicKey), + // ).length, + // ) + // .to.equal(1); + // }); + // + // it('Should return list of dialects sorted by time desc', async () => { + // // given + // console.log('Creating users'); + // const [user1, user2, user3] = await Promise.all([ + // createUser({ + // requestAirdrop: true, + // createMeta: true, + // }).then((it) => it.user), + // createUser({ + // requestAirdrop: true, + // createMeta: true, + // }).then((it) => it.user), + // createUser({ + // requestAirdrop: true, + // createMeta: true, + // }).then((it) => it.user), + // ]); + // console.log('Creating dialects'); + // // create first dialect and subscribe users + // const dialect1 = await createDialectAndSubscribeAllMembers( + // program, + // user1, + // user2, + // false, + // ); + // const dialect2 = await createDialectAndSubscribeAllMembers( + // program, + // user1, + // user3, + // false, + // ); + // // when + // const afterCreatingDialects = await getDialects(program, user1); + // await sleep(3000); // wait a bit to avoid equal timestamp, since since we get utc seconds as a timestamp + // await sendMessage( + // program, + // dialect1, + // user1, + // 'Dummy message to increment latest message timestamp', + // ); + // const afterSendingMessageToDialect1 = await getDialects(program, user1); + // await sleep(3000); // wait a bit to avoid equal timestamp, since since we get utc seconds as a timestamp + // await sendMessage( + // program, + // dialect2, + // user1, + // 'Dummy message to increment latest message timestamp', + // ); + // const afterSendingMessageToDialect2 = await getDialects(program, user1); + // // then + // // assert dialects before sending messages + // chai + // .expect(afterCreatingDialects.map((it) => it.publicKey)) + // .to.be.deep.eq([dialect2.publicKey, dialect1.publicKey]); // dialect 2 was created after dialect 1 + // // assert dialects after sending message to first dialect + // chai + // .expect(afterSendingMessageToDialect1.map((it) => it.publicKey)) + // .to.be.deep.eq([dialect1.publicKey, dialect2.publicKey]); + // // assert dialects after sending message to second dialect + // chai + // .expect(afterSendingMessageToDialect2.map((it) => it.publicKey)) + // .to.be.deep.eq([dialect2.publicKey, dialect1.publicKey]); + // }); + // + // it('Non-owners fail to delete the dialect', async () => { + // const dialect = await createDialect(program, owner, members); + // chai + // .expect(deleteDialect(program, dialect, writer)) + // .to.eventually.be.rejectedWith(Error); + // chai + // .expect(deleteDialect(program, dialect, nonmember)) + // .to.eventually.be.rejectedWith(Error); + // }); + // + // it('Owner deletes the dialect', async () => { + // const dialect = await createDialect(program, owner, members); + // await deleteDialect(program, dialect, owner); + // chai + // .expect(getDialectForMembers(program, members)) + // .to.eventually.be.rejectedWith(Error); + // }); + // + // it('Fail to subscribe a user twice to the same dialect (silent, noop)', async () => { + // const dialect = await createDialect(program, owner, members); + // await subscribeUser(program, dialect, writer.publicKey, owner); + // const metadata = await getMetadata(program, writer.publicKey); + // // subscribed once + // chai + // .expect( + // metadata.subscriptions.filter((s) => + // s.pubkey.equals(dialect.publicKey), + // ).length, + // ) + // .to.equal(1); + // chai + // .expect(subscribeUser(program, dialect, writer.publicKey, owner)) + // .to.be.rejectedWith(Error); + // // still subscribed just once + // chai + // .expect( + // metadata.subscriptions.filter((s) => + // s.pubkey.equals(dialect.publicKey), + // ).length, + // ) + // .to.equal(1); + // }); + // }); - writer1Dialect = await getDialectForMembers( - program, - members, - writer1.encryptionProps, - ); - writer2Dialect = await getDialectForMembers( - program, - members, - writer2.encryptionProps, - ); + // describe('Find dialects', () => { + // it('Can find all dialects filtering by user public key', async () => { + // // given + // const [user1, user2, user3] = await Promise.all([ + // createUser({ + // requestAirdrop: true, + // createMeta: false, + // }).then((it) => it.user), + // createUser({ + // requestAirdrop: true, + // createMeta: false, + // }).then((it) => it.user), + // createUser({ + // requestAirdrop: true, + // createMeta: false, + // }).then((it) => it.user), + // ]); + // const [user1User2Dialect, user1User3Dialect, user2User3Dialect] = + // await Promise.all([ + // createDialect(program, user1, [ + // { + // publicKey: user1.publicKey, + // scopes: [true, true], + // }, + // { + // publicKey: user2.publicKey, + // scopes: [false, true], + // }, + // ]), + // createDialect(program, user1, [ + // { + // publicKey: user1.publicKey, + // scopes: [true, true], + // }, + // { + // publicKey: user3.publicKey, + // scopes: [false, true], + // }, + // ]), + // createDialect(program, user2, [ + // { + // publicKey: user2.publicKey, + // scopes: [true, true], + // }, + // { + // publicKey: user3.publicKey, + // scopes: [false, true], + // }, + // ]), + // ]); + // // when + // const [ + // user1Dialects, + // user2Dialects, + // user3Dialects, + // nonExistingUserDialects, + // ] = await Promise.all([ + // findDialects(program, { + // userPk: user1.publicKey, + // }), + // findDialects(program, { + // userPk: user2.publicKey, + // }), + // findDialects(program, { + // userPk: user3.publicKey, + // }), + // findDialects(program, { + // userPk: anchor.web3.Keypair.generate().publicKey, + // }), + // ]); + // // then + // expect( + // user1Dialects.map((it) => it.publicKey), + // ).to.deep.contain.all.members([ + // user1User2Dialect.publicKey, + // user1User3Dialect.publicKey, + // ]); + // expect( + // user2Dialects.map((it) => it.publicKey), + // ).to.deep.contain.all.members([ + // user1User2Dialect.publicKey, + // user2User3Dialect.publicKey, + // ]); + // expect( + // user3Dialects.map((it) => it.publicKey), + // ).to.deep.contain.all.members([ + // user2User3Dialect.publicKey, + // user1User3Dialect.publicKey, + // ]); + // expect(nonExistingUserDialects.length).to.be.eq(0); + // }); + // }); - // then check writer1 dialect state - const message1Writer1 = writer1Dialect.dialect.messages[1]; - const message2Writer1 = writer1Dialect.dialect.messages[0]; - chai.expect(message1Writer1.text).to.be.eq(writer1Text); - chai.expect(message1Writer1.owner).to.be.deep.eq(writer1.user.publicKey); - chai.expect(message2Writer1.text).to.be.eq(writer2Text); - chai.expect(message2Writer1.owner).to.be.deep.eq(writer2.user.publicKey); - // then check writer2 dialect state - const message1Writer2 = writer2Dialect.dialect.messages[1]; - const message2Writer2 = writer2Dialect.dialect.messages[0]; - chai.expect(message1Writer2.text).to.be.eq(writer1Text); - chai.expect(message1Writer2.owner).to.be.deep.eq(writer1.user.publicKey); - chai.expect(message2Writer2.text).to.be.eq(writer2Text); - chai.expect(message2Writer2.owner).to.be.deep.eq(writer2.user.publicKey); - }); + // describe('Unencrypted messaging tests', () => { + // let owner: web3.Keypair; + // let writer: web3.Keypair; + // let nonmember: web3.Keypair; + // let members: Member[] = []; + // let dialect: DialectAccount; + // + // beforeEach(async () => { + // (owner = await createUser({ + // requestAirdrop: true, + // createMeta: true, + // }).then((it) => it.user)), + // (writer = await createUser({ + // requestAirdrop: true, + // createMeta: true, + // }).then((it) => it.user)), + // (nonmember = await createUser({ + // requestAirdrop: true, + // createMeta: false, + // }).then((it) => it.user)), + // (members = [ + // { + // publicKey: owner.publicKey, + // scopes: [true, false], // owner, read-only + // }, + // { + // publicKey: writer.publicKey, + // scopes: [false, true], // non-owner, read-write + // }, + // ]); + // dialect = await createDialect(program, owner, members, false); + // }); + // + // it('Message sender and receiver can read the message text and time', async () => { + // // given + // const dialect = await getDialectForMembers(program, members); + // const text = generateRandomText(256); + // // when + // await sendMessage(program, dialect, writer, text); + // // then + // const senderDialect = await getDialectForMembers( + // program, + // dialect.dialect.members, + // ); + // const message = senderDialect.dialect.messages[0]; + // chai.expect(message.text).to.be.eq(text); + // chai + // .expect(senderDialect.dialect.lastMessageTimestamp) + // .to.be.eq(message.timestamp); + // }); + // + // it('Anonymous user can read any of the messages', async () => { + // // given + // const senderDialect = await getDialectForMembers(program, members); + // const text = generateRandomText(256); + // await sendMessage(program, senderDialect, writer, text); + // // when / then + // const nonMemberDialect = await getDialectForMembers( + // program, + // dialect.dialect.members, + // ); + // const message = nonMemberDialect.dialect.messages[0]; + // chai.expect(message.text).to.be.eq(text); + // chai.expect(message.owner).to.be.deep.eq(writer.publicKey); + // chai + // .expect(nonMemberDialect.dialect.lastMessageTimestamp) + // .to.be.eq(message.timestamp); + // }); + // + // it('New messages overwrite old, retrieved messages are in order.', async () => { + // // emulate ideal message alignment withing buffer + // const rawBufferSize = 8192; + // const messagesPerDialect = 16; + // const numMessages = messagesPerDialect * 2; + // const salt = 3; + // const targetRawMessageSize = rawBufferSize / messagesPerDialect - salt; + // const timestampSize = 4; + // const ownerMemberIdxSize = 1; + // const messageSerializationOverhead = + // ITEM_METADATA_OVERHEAD + timestampSize + ownerMemberIdxSize; + // const targetTextSize = + // targetRawMessageSize - messageSerializationOverhead; + // const texts = Array(numMessages) + // .fill(0) + // .map(() => generateRandomText(targetTextSize)); + // for (let messageIdx = 0; messageIdx < numMessages; messageIdx++) { + // // verify last last N messages look correct + // const messageCounter = messageIdx + 1; + // const text = texts[messageIdx]; + // const dialect = await getDialectForMembers(program, members); + // console.log( + // `Sending message ${messageCounter}/${texts.length} + // len = ${text.length} + // idx: ${dialect.dialect.nextMessageIdx}`, + // ); + // await sendMessage(program, dialect, writer, text); + // const sliceStart = + // messageCounter <= messagesPerDialect + // ? 0 + // : messageCounter - messagesPerDialect; + // const expectedMessagesCount = Math.min( + // messageCounter, + // messagesPerDialect, + // ); + // const sliceEnd = sliceStart + expectedMessagesCount; + // const expectedMessages = texts.slice(sliceStart, sliceEnd).reverse(); + // const d = await getDialect(program, dialect.publicKey); + // const actualMessages = d.dialect.messages.map((m) => m.text); + // console.log(` msgs count after send: ${actualMessages.length}\n`); + // expect(actualMessages).to.be.deep.eq(expectedMessages); + // } + // }); + // + // it('Message text limit of 853 bytes can be sent/received', async () => { + // const maxMessageSizeBytes = 853; + // const texts = Array(30) + // .fill(0) + // .map(() => generateRandomText(maxMessageSizeBytes)); + // for (let messageIdx = 0; messageIdx < texts.length; messageIdx++) { + // const text = texts[messageIdx]; + // const messageCounter = messageIdx + 1; + // const dialect = await getDialectForMembers(program, members); + // console.log( + // `Sending message ${messageCounter}/${texts.length} + // len = ${text.length} + // idx: ${dialect.dialect.nextMessageIdx}`, + // ); + // // when + // await sendMessage(program, dialect, writer, text); + // const d = await getDialect(program, dialect.publicKey); + // const actualMessages = d.dialect.messages; + // const lastMessage = actualMessages[0]; + // console.log(` msgs count after send: ${actualMessages.length}\n`); + // // then + // expect(lastMessage.text).to.be.deep.eq(text); + // } + // }); + // }); - // This test was failing before changing nonce generation algorithm - it('2 writers can send a messages and read them when dialect state is not linearized before sending msg', async () => { - // given - const writer1 = await createUser({ - requestAirdrop: true, - createMeta: true, - }); - const writer2 = await createUser({ - requestAirdrop: true, - createMeta: true, - }); - members = [ - { - publicKey: writer1.user.publicKey, - scopes: [true, true], // owner, read-only - }, - { - publicKey: writer2.user.publicKey, - scopes: [false, true], // non-owner, read-write - }, - ]; - await createDialect(program, writer1.user, members, true); - // when - let writer1Dialect = await getDialectForMembers( - program, - members, - writer1.encryptionProps, - ); - let writer2Dialect = await getDialectForMembers( - program, - members, - writer2.encryptionProps, - ); // ensures no dialect state linearization - const writer1Text = generateRandomText(256); - await sendMessage( - program, - writer1Dialect, - writer1.user, - writer1Text, - writer1.encryptionProps, - ); - const writer2Text = generateRandomText(256); - await sendMessage( - program, - writer2Dialect, - writer2.user, - writer2Text, - writer2.encryptionProps, - ); + // describe('Encrypted messaging tests', () => { + // let owner: web3.Keypair; + // let ownerEncryptionProps: EncryptionProps; + // let writer: web3.Keypair; + // let writerEncryptionProps: EncryptionProps; + // let nonmember: web3.Keypair; + // let nonmemberEncryptionProps: EncryptionProps; + // let members: Member[] = []; + // let dialect: DialectAccount; + // + // beforeEach(async () => { + // const ownerUser = await createUser({ + // requestAirdrop: true, + // createMeta: true, + // }); + // owner = ownerUser.user; + // ownerEncryptionProps = ownerUser.encryptionProps; + // const writerUser = await createUser({ + // requestAirdrop: true, + // createMeta: true, + // }); + // writer = writerUser.user; + // writerEncryptionProps = writerUser.encryptionProps; + // const nonmemberUser = await createUser({ + // requestAirdrop: true, + // createMeta: false, + // }); + // nonmember = nonmemberUser.user; + // nonmemberEncryptionProps = nonmemberUser.encryptionProps; + // members = [ + // { + // publicKey: owner.publicKey, + // scopes: [true, false], // owner, read-only + // }, + // { + // publicKey: writer.publicKey, + // scopes: [false, true], // non-owner, read-write + // }, + // ]; + // dialect = await createDialect(program, owner, members, true); + // }); + // + // it('Message sender can send msg and then read the message text and time', async () => { + // // given + // const dialect = await getDialectForMembers( + // program, + // members, + // writerEncryptionProps, + // ); + // const text = generateRandomText(256); + // // when + // await sendMessage(program, dialect, writer, text, writerEncryptionProps); + // // then + // const senderDialect = await getDialectForMembers( + // program, + // dialect.dialect.members, + // writerEncryptionProps, + // ); + // const message = senderDialect.dialect.messages[0]; + // chai.expect(message.text).to.be.eq(text); + // chai.expect(message.owner).to.be.deep.eq(writer.publicKey); + // chai + // .expect(senderDialect.dialect.lastMessageTimestamp) + // .to.be.eq(message.timestamp); + // }); + // + // it('Message receiver can read the message text and time sent by sender', async () => { + // // given + // const senderDialect = await getDialectForMembers( + // program, + // members, + // writerEncryptionProps, + // ); + // const text = generateRandomText(256); + // // when + // await sendMessage( + // program, + // senderDialect, + // writer, + // text, + // writerEncryptionProps, + // ); + // // then + // const receiverDialect = await getDialectForMembers( + // program, + // dialect.dialect.members, + // ownerEncryptionProps, + // ); + // const message = receiverDialect.dialect.messages[0]; + // chai.expect(message.text).to.be.eq(text); + // chai.expect(message.owner).to.be.deep.eq(writer.publicKey); + // chai + // .expect(receiverDialect.dialect.lastMessageTimestamp) + // .to.be.eq(message.timestamp); + // }); + // + // it("Non-member can't read (decrypt) any of the messages", async () => { + // // given + // const senderDialect = await getDialectForMembers( + // program, + // members, + // writerEncryptionProps, + // ); + // const text = generateRandomText(256); + // await sendMessage( + // program, + // senderDialect, + // writer, + // text, + // writerEncryptionProps, + // ); + // // when / then + // expect( + // getDialectForMembers( + // program, + // dialect.dialect.members, + // nonmemberEncryptionProps, + // ), + // ).to.eventually.be.rejected; + // }); + // + // it('New messages overwrite old, retrieved messages are in order.', async () => { + // // emulate ideal message alignment withing buffer + // const rawBufferSize = 8192; + // const messagesPerDialect = 16; + // const numMessages = messagesPerDialect * 2; + // const salt = 3; + // const targetRawMessageSize = rawBufferSize / messagesPerDialect - salt; + // const timestampSize = 4; + // const ownerMemberIdxSize = 1; + // const messageSerializationOverhead = + // ITEM_METADATA_OVERHEAD + + // ENCRYPTION_OVERHEAD_BYTES + + // NONCE_SIZE_BYTES + + // timestampSize + + // ownerMemberIdxSize; + // const targetTextSize = + // targetRawMessageSize - messageSerializationOverhead; + // const texts = Array(numMessages) + // .fill(0) + // .map(() => generateRandomText(targetTextSize)); + // for (let messageIdx = 0; messageIdx < numMessages; messageIdx++) { + // // verify last last N messages look correct + // const messageCounter = messageIdx + 1; + // const text = texts[messageIdx]; + // const dialect = await getDialectForMembers( + // program, + // members, + // writerEncryptionProps, + // ); + // console.log( + // `Sending message ${messageCounter}/${texts.length} + // len = ${text.length} + // idx: ${dialect.dialect.nextMessageIdx}`, + // ); + // await sendMessage( + // program, + // dialect, + // writer, + // text, + // writerEncryptionProps, + // ); + // const sliceStart = + // messageCounter <= messagesPerDialect + // ? 0 + // : messageCounter - messagesPerDialect; + // const expectedMessagesCount = Math.min( + // messageCounter, + // messagesPerDialect, + // ); + // const sliceEnd = sliceStart + expectedMessagesCount; + // const expectedMessages = texts.slice(sliceStart, sliceEnd).reverse(); + // const d = await getDialect( + // program, + // dialect.publicKey, + // writerEncryptionProps, + // ); + // const actualMessages = d.dialect.messages.map((m) => m.text); + // console.log(` msgs count after send: ${actualMessages.length}\n`); + // expect(actualMessages).to.be.deep.eq(expectedMessages); + // } + // }); + // + // it('Send/receive random size messages.', async () => { + // const texts = Array(32) + // .fill(0) + // .map(() => generateRandomText(randomInt(256, 512))); + // for (let messageIdx = 0; messageIdx < texts.length; messageIdx++) { + // const text = texts[messageIdx]; + // const messageCounter = messageIdx + 1; + // const dialect = await getDialectForMembers( + // program, + // members, + // writerEncryptionProps, + // ); + // console.log( + // `Sending message ${messageCounter}/${texts.length} + // len = ${text.length} + // idx: ${dialect.dialect.nextMessageIdx}`, + // ); + // // when + // await sendMessage( + // program, + // dialect, + // writer, + // text, + // writerEncryptionProps, + // ); + // const d = await getDialect( + // program, + // dialect.publicKey, + // writerEncryptionProps, + // ); + // const actualMessages = d.dialect.messages; + // const lastMessage = actualMessages[0]; + // console.log(` msgs count after send: ${actualMessages.length}\n`); + // // then + // expect(lastMessage.text).to.be.deep.eq(text); + // } + // }); + // + // /* UTF-8 encoding summary: + // - ASCII characters are encoded using 1 byte + // - Roman, Greek, Cyrillic, Coptic, Armenian, Hebrew, Arabic characters are encoded using 2 bytes + // - Chinese and Japanese among others are encoded using 3 bytes + // - Emoji are encoded using 4 bytes + // A note about message length limit and summary: + // - len >= 814 hits max transaction size limit = 1232 bytes https://docs.solana.com/ru/proposals/transactions-v2 + // - => best case: 813 symbols per msg (ascii only) + // - => worst case: 203 symbols (e.g. emoji only) + // - => average case depends on character set, see details below: + // ---- ASCII: ±800 characters + // ---- Roman, Greek, Cyrillic, Coptic, Armenian, Hebrew, Arabic: ± 406 characters + // ---- Chinese and japanese: ± 270 characters + // ---- Emoji: ± 203 characters*/ + // it('Message text limit of 813 bytes can be sent/received', async () => { + // const maxMessageSizeBytes = 813; + // const texts = Array(30) + // .fill(0) + // .map(() => generateRandomText(maxMessageSizeBytes)); + // for (let messageIdx = 0; messageIdx < texts.length; messageIdx++) { + // const text = texts[messageIdx]; + // const messageCounter = messageIdx + 1; + // const dialect = await getDialectForMembers( + // program, + // members, + // writerEncryptionProps, + // ); + // console.log( + // `Sending message ${messageCounter}/${texts.length} + // len = ${text.length} + // idx: ${dialect.dialect.nextMessageIdx}`, + // ); + // // when + // await sendMessage( + // program, + // dialect, + // writer, + // text, + // writerEncryptionProps, + // ); + // const d = await getDialect( + // program, + // dialect.publicKey, + // writerEncryptionProps, + // ); + // const actualMessages = d.dialect.messages; + // const lastMessage = actualMessages[0]; + // console.log(` msgs count after send: ${actualMessages.length}\n`); + // // then + // expect(lastMessage.text).to.be.deep.eq(text); + // } + // }); + // + // it('2 writers can send a messages and read them when dialect state is linearized before sending msg', async () => { + // // given + // const writer1 = await createUser({ + // requestAirdrop: true, + // createMeta: true, + // }); + // const writer2 = await createUser({ + // requestAirdrop: true, + // createMeta: true, + // }); + // members = [ + // { + // publicKey: writer1.user.publicKey, + // scopes: [true, true], // owner, read-only + // }, + // { + // publicKey: writer2.user.publicKey, + // scopes: [false, true], // non-owner, read-write + // }, + // ]; + // await createDialect(program, writer1.user, members, true); + // // when + // let writer1Dialect = await getDialectForMembers( + // program, + // members, + // writer1.encryptionProps, + // ); + // const writer1Text = generateRandomText(256); + // await sendMessage( + // program, + // writer1Dialect, + // writer1.user, + // writer1Text, + // writer1.encryptionProps, + // ); + // let writer2Dialect = await getDialectForMembers( + // program, + // members, + // writer2.encryptionProps, + // ); // ensures dialect state linearization + // const writer2Text = generateRandomText(256); + // await sendMessage( + // program, + // writer2Dialect, + // writer2.user, + // writer2Text, + // writer2.encryptionProps, + // ); + // + // writer1Dialect = await getDialectForMembers( + // program, + // members, + // writer1.encryptionProps, + // ); + // writer2Dialect = await getDialectForMembers( + // program, + // members, + // writer2.encryptionProps, + // ); + // + // // then check writer1 dialect state + // const message1Writer1 = writer1Dialect.dialect.messages[1]; + // const message2Writer1 = writer1Dialect.dialect.messages[0]; + // chai.expect(message1Writer1.text).to.be.eq(writer1Text); + // chai.expect(message1Writer1.owner).to.be.deep.eq(writer1.user.publicKey); + // chai.expect(message2Writer1.text).to.be.eq(writer2Text); + // chai.expect(message2Writer1.owner).to.be.deep.eq(writer2.user.publicKey); + // // then check writer2 dialect state + // const message1Writer2 = writer2Dialect.dialect.messages[1]; + // const message2Writer2 = writer2Dialect.dialect.messages[0]; + // chai.expect(message1Writer2.text).to.be.eq(writer1Text); + // chai.expect(message1Writer2.owner).to.be.deep.eq(writer1.user.publicKey); + // chai.expect(message2Writer2.text).to.be.eq(writer2Text); + // chai.expect(message2Writer2.owner).to.be.deep.eq(writer2.user.publicKey); + // }); + // + // // This test was failing before changing nonce generation algorithm + // it('2 writers can send a messages and read them when dialect state is not linearized before sending msg', async () => { + // // given + // const writer1 = await createUser({ + // requestAirdrop: true, + // createMeta: true, + // }); + // const writer2 = await createUser({ + // requestAirdrop: true, + // createMeta: true, + // }); + // members = [ + // { + // publicKey: writer1.user.publicKey, + // scopes: [true, true], // owner, read-only + // }, + // { + // publicKey: writer2.user.publicKey, + // scopes: [false, true], // non-owner, read-write + // }, + // ]; + // await createDialect(program, writer1.user, members, true); + // // when + // let writer1Dialect = await getDialectForMembers( + // program, + // members, + // writer1.encryptionProps, + // ); + // let writer2Dialect = await getDialectForMembers( + // program, + // members, + // writer2.encryptionProps, + // ); // ensures no dialect state linearization + // const writer1Text = generateRandomText(256); + // await sendMessage( + // program, + // writer1Dialect, + // writer1.user, + // writer1Text, + // writer1.encryptionProps, + // ); + // const writer2Text = generateRandomText(256); + // await sendMessage( + // program, + // writer2Dialect, + // writer2.user, + // writer2Text, + // writer2.encryptionProps, + // ); + // + // writer1Dialect = await getDialectForMembers( + // program, + // members, + // writer1.encryptionProps, + // ); + // writer2Dialect = await getDialectForMembers( + // program, + // members, + // writer2.encryptionProps, + // ); + // + // // then check writer1 dialect state + // const message1Writer1 = writer1Dialect.dialect.messages[1]; + // const message2Writer1 = writer1Dialect.dialect.messages[0]; + // chai.expect(message1Writer1.text).to.be.eq(writer1Text); + // chai.expect(message1Writer1.owner).to.be.deep.eq(writer1.user.publicKey); + // chai.expect(message2Writer1.text).to.be.eq(writer2Text); + // chai.expect(message2Writer1.owner).to.be.deep.eq(writer2.user.publicKey); + // // then check writer2 dialect state + // const message1Writer2 = writer2Dialect.dialect.messages[1]; + // const message2Writer2 = writer2Dialect.dialect.messages[0]; + // chai.expect(message1Writer2.text).to.be.eq(writer1Text); + // chai.expect(message1Writer2.owner).to.be.deep.eq(writer1.user.publicKey); + // chai.expect(message2Writer2.text).to.be.eq(writer2Text); + // chai.expect(message2Writer2.owner).to.be.deep.eq(writer2.user.publicKey); + // }); + // }); - writer1Dialect = await getDialectForMembers( - program, - members, - writer1.encryptionProps, - ); - writer2Dialect = await getDialectForMembers( - program, - members, - writer2.encryptionProps, - ); + // describe('Subscription tests', () => { + // let owner: web3.Keypair; + // let writer: web3.Keypair; + // + // beforeEach(async () => { + // owner = await createUser({ + // requestAirdrop: true, + // createMeta: false, + // }).then((it) => it.user); + // writer = await createUser({ + // requestAirdrop: true, + // createMeta: false, + // }).then((it) => it.user); + // }); + // + // it('Can subscribe to events and receive them and unsubscribe', async () => { + // // given + // const eventsAccumulator: Event[] = []; + // const expectedEvents = 8; + // const countDownLatch = new CountDownLatch(expectedEvents); + // const subscription = await subscribeToEvents(program, async (it) => { + // console.log('event', it); + // countDownLatch.countDown(); + // return eventsAccumulator.push(it); + // }); + // // when + // await createMetadata(program, owner); // 1 event + // await createMetadata(program, writer); // 1 event + // const dialectAccount = await createDialectAndSubscribeAllMembers( + // program, + // owner, + // writer, + // false, + // ); // 3 events + // await deleteMetadata(program, owner); // 1 event + // await deleteMetadata(program, writer); // 1 event + // await deleteDialect(program, dialectAccount, owner); // 1 event + // await countDownLatch.await(5000); + // await subscription.unsubscribe(); + // // events below should be ignored + // await createMetadata(program, owner); + // await createMetadata(program, writer); + // // then + // chai.expect(eventsAccumulator.length).to.be.eq(expectedEvents); + // }); + // }); - // then check writer1 dialect state - const message1Writer1 = writer1Dialect.dialect.messages[1]; - const message2Writer1 = writer1Dialect.dialect.messages[0]; - chai.expect(message1Writer1.text).to.be.eq(writer1Text); - chai.expect(message1Writer1.owner).to.be.deep.eq(writer1.user.publicKey); - chai.expect(message2Writer1.text).to.be.eq(writer2Text); - chai.expect(message2Writer1.owner).to.be.deep.eq(writer2.user.publicKey); - // then check writer2 dialect state - const message1Writer2 = writer2Dialect.dialect.messages[1]; - const message2Writer2 = writer2Dialect.dialect.messages[0]; - chai.expect(message1Writer2.text).to.be.eq(writer1Text); - chai.expect(message1Writer2.owner).to.be.deep.eq(writer1.user.publicKey); - chai.expect(message2Writer2.text).to.be.eq(writer2Text); - chai.expect(message2Writer2.owner).to.be.deep.eq(writer2.user.publicKey); - }); - }); - - describe('Subscription tests', () => { - let owner: web3.Keypair; - let writer: web3.Keypair; - - beforeEach(async () => { - owner = await createUser({ - requestAirdrop: true, - createMeta: false, - }).then((it) => it.user); - writer = await createUser({ - requestAirdrop: true, - createMeta: false, - }).then((it) => it.user); - }); - - it('Can subscribe to events and receive them and unsubscribe', async () => { - // given - const eventsAccumulator: Event[] = []; - const expectedEvents = 8; - const countDownLatch = new CountDownLatch(expectedEvents); - const subscription = await subscribeToEvents(program, async (it) => { - console.log('event', it); - countDownLatch.countDown(); - return eventsAccumulator.push(it); - }); - // when - await createMetadata(program, owner); // 1 event - await createMetadata(program, writer); // 1 event - const dialectAccount = await createDialectAndSubscribeAllMembers( - program, - owner, - writer, - false, - ); // 3 events - await deleteMetadata(program, owner); // 1 event - await deleteMetadata(program, writer); // 1 event - await deleteDialect(program, dialectAccount, owner); // 1 event - await countDownLatch.await(5000); - await subscription.unsubscribe(); - // events below should be ignored - await createMetadata(program, owner); - await createMetadata(program, writer); - // then - chai.expect(eventsAccumulator.length).to.be.eq(expectedEvents); - }); - }); + interface User { + program: Program; + wallet: Wallet; + publicKey: PublicKey; + encryptionProps: EncryptionProps; + } async function createUser( { requestAirdrop, createMeta }: CreateUserOptions = { requestAirdrop: true, createMeta: true, }, - ) { + ): Promise { const keypair = web3.Keypair.generate(); const wallet = Wallet_.embedded(keypair.secretKey); const RPC_URL = process.env.RPC_URL || 'http://localhost:8899'; @@ -1098,9 +1090,10 @@ describe('Protocol v1 test', () => { new web3.PublicKey(DIALECT_PROGRAM_ADDRESS), dialectProvider, ); + const publicKey = wallet.publicKey; if (requestAirdrop) { const airDropRequest = await connection.requestAirdrop( - wallet.publicKey, + publicKey, 10 * web3.LAMPORTS_PER_SOL, ); await connection.confirmTransaction(airDropRequest); @@ -1109,13 +1102,13 @@ describe('Protocol v1 test', () => { await createMetadata(program); } const encryptionProps = { - ed25519PublicKey: keypair.publicKey.toBytes(), + ed25519PublicKey: publicKey.toBytes(), diffieHellmanKeyPair: ed25519KeyPairToCurve25519({ publicKey: keypair.publicKey.toBytes(), secretKey: keypair.secretKey, }), }; - return { program, encryptionProps }; + return { program, wallet, publicKey, encryptionProps }; } }); From 8642bce1f7eab31ce78af233e46ef183e47e265a Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Fri, 8 Apr 2022 16:33:35 +0400 Subject: [PATCH 09/10] Working on fixing tests --- README.md | 2 +- src/api/index.ts | 10 +- tests/test-v1.ts | 601 +++++++++++++++++++++++------------------------ 3 files changed, 300 insertions(+), 313 deletions(-) diff --git a/README.md b/README.md index f45a886..b8abdf9 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Dialect is a smart messaging protocol for dapp notifications and wallet-to-wallet messaging on the Solana Blockchain. Dialect works by decorating on-chain resources, or sets of resources, with publish-subscribe (pub-sub) messaging capabilities. This is accomplished by creating a PDA whose seeds are the (lexically sorted) resources' public keys. Each pub-sub messaging PDA is called a _dialect_. - + Dialect `v0` currently supports one-to-one messaging between wallets, which powers both dapp notifications as well as user-to-user chat. Future versions of Dialect will also support one-to-many and many-to-many messaging. This repository contains both the Dialect rust programs (protocol), in Anchor, as well as a typescript client, published to npm as `@dialectlabs/web3`. diff --git a/src/api/index.ts b/src/api/index.ts index cbc0429..0e6de27 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -181,9 +181,7 @@ export async function createMetadata( return await getMetadata(program, publicKey); } -export async function deleteMetadata( - program: anchor.Program, -): Promise { +export async function deleteMetadata(program: anchor.Program): Promise { const wallet = program.provider.wallet; const publicKey = wallet.publicKey; const [metadataAddress, metadataNonce] = await getMetadataProgramAddress( @@ -320,10 +318,10 @@ export async function getDialect( export async function getDialects( program: anchor.Program, - user: Wallet, + user: PublicKey | Wallet, // TODO: why we need wallet here? encryptionProps?: EncryptionProps, ): Promise { - const metadata = await getMetadata(program, user.publicKey); + const metadata = await getMetadata(program, user); const enabledSubscriptions = metadata.subscriptions.filter( (it) => it.enabled, ); @@ -467,7 +465,6 @@ export async function sendMessage( text: string, encryptionProps?: EncryptionProps, ): Promise { - const wallet = program.provider.wallet; const [dialectPublicKey, nonce] = await getDialectProgramAddress( program, dialect.members, @@ -480,6 +477,7 @@ export async function sendMessage( encryptionProps, ); const serializedText = textSerde.serialize(text); + const wallet = program.provider.wallet; await program.rpc.sendMessage( new anchor.BN(nonce), Buffer.from(serializedText), diff --git a/tests/test-v1.ts b/tests/test-v1.ts index 428291d..ec5b7c0 100644 --- a/tests/test-v1.ts +++ b/tests/test-v1.ts @@ -1,5 +1,5 @@ import * as anchor from '@project-serum/anchor'; -import { Idl, Program, Provider } from '@project-serum/anchor'; +import { AnchorError, Idl, Program, Provider } from '@project-serum/anchor'; import * as web3 from '@solana/web3.js'; import { PublicKey } from '@solana/web3.js'; import chai, { expect } from 'chai'; @@ -7,16 +7,23 @@ import chaiAsPromised from 'chai-as-promised'; import { createDialect, createMetadata, + deleteDialect, deleteMetadata, + getDialectForMembers, + getDialectProgramAddress, + getDialects, getMetadata, Member, + sendMessage, subscribeUser, } from '../src/api'; -import { idl, programs, Wallet_ } from '../src/utils'; +import { idl, programs, sleep, Wallet_ } from '../src/utils'; import { ed25519KeyPairToCurve25519 } from '../src/utils/ecdh-encryption'; import { Wallet } from '../src/utils/Wallet'; import { EncryptionProps } from '../src/api/text-serde'; +import process from 'process'; +process.env.ANCHOR_WALLET = '/Users/tsmbl/.config/solana/id.json'; chai.use(chaiAsPromised); anchor.setProvider(anchor.Provider.local()); @@ -64,294 +71,276 @@ describe('Protocol v1 test', () => { }); }); - // describe('Dialect initialization tests', () => { - // let owner: Program; - // let writer: Program; - // let nonmember: Program; - // - // let members: Member[] = []; - // - // beforeEach(async () => { - // owner = ( - // await createUser({ - // requestAirdrop: true, - // createMeta: true, - // }) - // ).program; - // writer = ( - // await createUser({ - // requestAirdrop: true, - // createMeta: true, - // }) - // ).program; - // nonmember = ( - // await createUser({ - // requestAirdrop: true, - // createMeta: false, - // }) - // ).program; - // members = [ - // { - // publicKey: owner.provider.wallet.publicKey, - // scopes: [true, false], // owner, read-only - // }, - // { - // publicKey: writer.publicKey, - // scopes: [false, true], // non-owner, read-write - // }, - // ]; - // }); - // - // it('Confirm only each user (& dialect) can read encrypted device tokens', async () => { - // // TODO: Implement - // chai.expect(true).to.be.true; - // }); - // - // it("Fail to create a dialect if the owner isn't a member with admin privileges", async () => { - // try { - // await createDialect(program, nonmember, members, true); - // chai.assert( - // false, - // "Creating a dialect whose owner isn't a member should fail.", - // ); - // } catch (e) { - // chai.assert( - // (e as AnchorError).message.includes( - // 'The dialect owner must be a member with admin privileges.', - // ), - // ); - // } - // - // try { - // // TODO: write this in a nicer way - // await createDialect(program, writer, members, true); - // chai.assert( - // false, - // "Creating a dialect whose owner isn't a member should fail.", - // ); - // } catch (e) { - // chai.assert( - // (e as AnchorError).message.includes( - // 'The dialect owner must be a member with admin privileges.', - // ), - // ); - // } - // }); - // - // it('Fail to create a dialect for unsorted members', async () => { - // // use custom unsorted version of createDialect for unsorted members - // const unsortedMembers = members.sort( - // (a, b) => -a.publicKey.toBuffer().compare(b.publicKey.toBuffer()), - // ); - // const [publicKey, nonce] = await getDialectProgramAddress( - // program, - // unsortedMembers, - // ); - // // TODO: assert owner in members - // const keyedMembers = unsortedMembers.reduce( - // (ms, m, idx) => ({ ...ms, [`member${idx}`]: m.publicKey }), - // {}, - // ); - // chai - // .expect( - // program.rpc.createDialect( - // new anchor.BN(nonce), - // members.map((m) => m.scopes), - // { - // accounts: { - // dialect: publicKey, - // owner: owner.publicKey, - // ...keyedMembers, - // rent: anchor.web3.SYSVAR_RENT_PUBKEY, - // systemProgram: anchor.web3.SystemProgram.programId, - // }, - // signers: [owner], - // }, - // ), - // ) - // .to.eventually.be.rejectedWith(Error); - // }); - // - // it('Create encrypted dialect for 2 members, with owner and write scopes, respectively', async () => { - // const dialectAccount = await createDialect(program, owner, members, true); - // expect(dialectAccount.dialect.encrypted).to.be.true; - // }); - // - // it('Create unencrypted dialect for 2 members, with owner and write scopes, respectively', async () => { - // const dialectAccount = await createDialect( - // program, - // owner, - // members, - // false, - // ); - // expect(dialectAccount.dialect.encrypted).to.be.false; - // }); - // - // it('Creates unencrypted dialect by default', async () => { - // const dialectAccount = await createDialect(program, owner, members); - // expect(dialectAccount.dialect.encrypted).to.be.false; - // }); - // - // it('Fail to create a second dialect for the same members', async () => { - // chai - // .expect(createDialect(program, owner, members)) - // .to.eventually.be.rejectedWith(Error); - // }); - // - // it('Fail to create a dialect for duplicate members', async () => { - // const duplicateMembers = [ - // { publicKey: owner.publicKey, scopes: [true, true] } as Member, - // { publicKey: owner.publicKey, scopes: [true, true] } as Member, - // ]; - // chai - // .expect(createDialect(program, owner, duplicateMembers)) - // .to.be.rejectedWith(Error); - // }); - // - // it('Find a dialect for a given member pair, verify correct scopes.', async () => { - // await createDialect(program, owner, members); - // const dialect = await getDialectForMembers(program, members); - // members.every((m, i) => - // expect( - // m.publicKey.equals(dialect.dialect.members[i].publicKey) && - // m.scopes.every( - // (s, j) => s === dialect.dialect.members[i].scopes[j], - // ), - // ), - // ); - // }); - // - // it('Subscribe users to dialect', async () => { - // const dialect = await createDialect(program, owner, members); - // // owner subscribes themselves - // await subscribeUser(program, dialect, owner.publicKey, owner); - // // owner subscribes writer - // await subscribeUser(program, dialect, writer.publicKey, owner); - // const ownerMeta = await getMetadata(program, owner.publicKey); - // const writerMeta = await getMetadata(program, writer.publicKey); - // chai - // .expect( - // ownerMeta.subscriptions.filter((s) => - // s.pubkey.equals(dialect.publicKey), - // ).length, - // ) - // .to.equal(1); - // chai - // .expect( - // writerMeta.subscriptions.filter((s) => - // s.pubkey.equals(dialect.publicKey), - // ).length, - // ) - // .to.equal(1); - // }); - // - // it('Should return list of dialects sorted by time desc', async () => { - // // given - // console.log('Creating users'); - // const [user1, user2, user3] = await Promise.all([ - // createUser({ - // requestAirdrop: true, - // createMeta: true, - // }).then((it) => it.user), - // createUser({ - // requestAirdrop: true, - // createMeta: true, - // }).then((it) => it.user), - // createUser({ - // requestAirdrop: true, - // createMeta: true, - // }).then((it) => it.user), - // ]); - // console.log('Creating dialects'); - // // create first dialect and subscribe users - // const dialect1 = await createDialectAndSubscribeAllMembers( - // program, - // user1, - // user2, - // false, - // ); - // const dialect2 = await createDialectAndSubscribeAllMembers( - // program, - // user1, - // user3, - // false, - // ); - // // when - // const afterCreatingDialects = await getDialects(program, user1); - // await sleep(3000); // wait a bit to avoid equal timestamp, since since we get utc seconds as a timestamp - // await sendMessage( - // program, - // dialect1, - // user1, - // 'Dummy message to increment latest message timestamp', - // ); - // const afterSendingMessageToDialect1 = await getDialects(program, user1); - // await sleep(3000); // wait a bit to avoid equal timestamp, since since we get utc seconds as a timestamp - // await sendMessage( - // program, - // dialect2, - // user1, - // 'Dummy message to increment latest message timestamp', - // ); - // const afterSendingMessageToDialect2 = await getDialects(program, user1); - // // then - // // assert dialects before sending messages - // chai - // .expect(afterCreatingDialects.map((it) => it.publicKey)) - // .to.be.deep.eq([dialect2.publicKey, dialect1.publicKey]); // dialect 2 was created after dialect 1 - // // assert dialects after sending message to first dialect - // chai - // .expect(afterSendingMessageToDialect1.map((it) => it.publicKey)) - // .to.be.deep.eq([dialect1.publicKey, dialect2.publicKey]); - // // assert dialects after sending message to second dialect - // chai - // .expect(afterSendingMessageToDialect2.map((it) => it.publicKey)) - // .to.be.deep.eq([dialect2.publicKey, dialect1.publicKey]); - // }); - // - // it('Non-owners fail to delete the dialect', async () => { - // const dialect = await createDialect(program, owner, members); - // chai - // .expect(deleteDialect(program, dialect, writer)) - // .to.eventually.be.rejectedWith(Error); - // chai - // .expect(deleteDialect(program, dialect, nonmember)) - // .to.eventually.be.rejectedWith(Error); - // }); - // - // it('Owner deletes the dialect', async () => { - // const dialect = await createDialect(program, owner, members); - // await deleteDialect(program, dialect, owner); - // chai - // .expect(getDialectForMembers(program, members)) - // .to.eventually.be.rejectedWith(Error); - // }); - // - // it('Fail to subscribe a user twice to the same dialect (silent, noop)', async () => { - // const dialect = await createDialect(program, owner, members); - // await subscribeUser(program, dialect, writer.publicKey, owner); - // const metadata = await getMetadata(program, writer.publicKey); - // // subscribed once - // chai - // .expect( - // metadata.subscriptions.filter((s) => - // s.pubkey.equals(dialect.publicKey), - // ).length, - // ) - // .to.equal(1); - // chai - // .expect(subscribeUser(program, dialect, writer.publicKey, owner)) - // .to.be.rejectedWith(Error); - // // still subscribed just once - // chai - // .expect( - // metadata.subscriptions.filter((s) => - // s.pubkey.equals(dialect.publicKey), - // ).length, - // ) - // .to.equal(1); - // }); - // }); + describe('Dialect initialization tests', () => { + let owner: User; + let writer: User; + let nonmember: User; + + let members: Member[] = []; + + beforeEach(async () => { + owner = await createUser({ + requestAirdrop: true, + createMeta: true, + }); + writer = await createUser({ + requestAirdrop: true, + createMeta: true, + }); + nonmember = await createUser({ + requestAirdrop: true, + createMeta: false, + }); + members = [ + { + publicKey: owner.publicKey, + scopes: [true, false], // owner, read-only + }, + { + publicKey: writer.publicKey, + scopes: [false, true], // non-owner, read-write + }, + ]; + }); + + it('Confirm only each user (& dialect) can read encrypted device tokens', async () => { + // TODO: Implement + chai.expect(true).to.be.true; + }); + + it("Fail to create a dialect if the owner isn't a member with admin privileges", async () => { + try { + await createDialect(nonmember.program, members); + chai.assert( + false, + "Creating a dialect whose owner isn't a member should fail.", + ); + } catch (e) { + chai.assert( + (e as AnchorError).message.includes( + 'The dialect owner must be a member with admin privileges.', + ), + ); + } + + try { + // TODO: write this in a nicer way + await createDialect(writer.program, members); + chai.assert( + false, + "Creating a dialect whose owner isn't a member should fail.", + ); + } catch (e) { + chai.assert( + (e as AnchorError).message.includes( + 'The dialect owner must be a member with admin privileges.', + ), + ); + } + }); + + it('Fail to create a dialect for unsorted members', async () => { + // use custom unsorted version of createDialect for unsorted members + const unsortedMembers = members.sort( + (a, b) => -a.publicKey.toBuffer().compare(b.publicKey.toBuffer()), + ); + const [publicKey, nonce] = await getDialectProgramAddress( + writer.program, + unsortedMembers, + ); + // TODO: assert owner in members + const keyedMembers = unsortedMembers.reduce( + (ms, m, idx) => ({ ...ms, [`member${idx}`]: m.publicKey }), + {}, + ); + chai + .expect( + writer.program.rpc.createDialect( + new anchor.BN(nonce), + members.map((m) => m.scopes), + { + accounts: { + dialect: publicKey, + owner: owner.publicKey, + ...keyedMembers, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }, + }, + ), + ) + .to.eventually.be.rejectedWith(Error); + }); + + it('Create encrypted dialect for 2 members, with owner and write scopes, respectively', async () => { + const dialectAccount = await createDialect(owner.program, members, true); + expect(dialectAccount.dialect.encrypted).to.be.true; + }); + + it('Create unencrypted dialect for 2 members, with owner and write scopes, respectively', async () => { + const dialectAccount = await createDialect(owner.program, members, false); + expect(dialectAccount.dialect.encrypted).to.be.false; + }); + + it('Creates unencrypted dialect by default', async () => { + const dialectAccount = await createDialect(owner.program, members); + expect(dialectAccount.dialect.encrypted).to.be.false; + }); + + it('Fail to create a second dialect for the same members', async () => { + await createDialect(owner.program, members); + chai + .expect(createDialect(owner.program, members)) + .to.eventually.be.rejectedWith(Error); + }); + + it('Fail to create a dialect for duplicate members', async () => { + const duplicateMembers = [ + { publicKey: owner.publicKey, scopes: [true, true] } as Member, + { publicKey: owner.publicKey, scopes: [true, true] } as Member, + ]; + chai + .expect(createDialect(owner.program, duplicateMembers)) + .to.be.rejectedWith(Error); + }); + + it('Find a dialect for a given member pair, verify correct scopes.', async () => { + await createDialect(owner.program, members); + const dialect = await getDialectForMembers(owner.program, members); + members.every((m, i) => + expect( + m.publicKey.equals(dialect.dialect.members[i].publicKey) && + m.scopes.every( + (s, j) => s === dialect.dialect.members[i].scopes[j], + ), + ), + ); + }); + + // it('Subscribe users to dialect', async () => { + // const dialect = await createDialect(owner.program, members); + // // owner subscribes themselves + // await subscribeUser(program, dialect, owner.publicKey, owner); + // // owner subscribes writer + // await subscribeUser(program, dialect, writer.publicKey, owner); + // const ownerMeta = await getMetadata(program, owner.publicKey); + // const writerMeta = await getMetadata(program, writer.publicKey); + // chai + // .expect( + // ownerMeta.subscriptions.filter((s) => + // s.pubkey.equals(dialect.publicKey), + // ).length, + // ) + // .to.equal(1); + // chai + // .expect( + // writerMeta.subscriptions.filter((s) => + // s.pubkey.equals(dialect.publicKey), + // ).length, + // ) + // .to.equal(1); + // }); + + it('Should return list of dialects sorted by time desc', async () => { + // given + console.log('Creating users'); + const [user1, user2, user3] = await Promise.all([ + createUser(), + createUser(), + createUser(), + ]); + // create first dialect and subscribe users + const dialect1 = await createDialectAndSubscribeAllMembers( + user1, + user2.publicKey, + ); + const dialect2 = await createDialectAndSubscribeAllMembers( + user1, + user3.publicKey, + ); + // when + const afterCreatingDialects = await getDialects( + user1.program, + user1.publicKey, + ); + await sleep(3000); // wait a bit to avoid equal timestamp, since since we get utc seconds as a timestamp + await sendMessage( + user1.program, + dialect1, + 'Dummy message to increment latest message timestamp', + ); + const afterSendingMessageToDialect1 = await getDialects( + user1.program, + user1.publicKey, + ); + await sleep(3000); // wait a bit to avoid equal timestamp, since since we get utc seconds as a timestamp + await sendMessage( + user1.program, + dialect2, + 'Dummy message to increment latest message timestamp', + ); + const afterSendingMessageToDialect2 = await getDialects( + user1.program, + user1.publicKey, + ); + // then + // assert dialects before sending messages + chai + .expect(afterCreatingDialects.map((it) => it.publicKey)) + .to.be.deep.eq([dialect2.publicKey, dialect1.publicKey]); // dialect 2 was created after dialect 1 + // assert dialects after sending message to first dialect + chai + .expect(afterSendingMessageToDialect1.map((it) => it.publicKey)) + .to.be.deep.eq([dialect1.publicKey, dialect2.publicKey]); + // assert dialects after sending message to second dialect + chai + .expect(afterSendingMessageToDialect2.map((it) => it.publicKey)) + .to.be.deep.eq([dialect2.publicKey, dialect1.publicKey]); + }); + + it('Non-owners fail to delete the dialect', async () => { + const dialect = await createDialect(owner.program, members); + chai + .expect(deleteDialect(writer.program, dialect)) + .to.eventually.be.rejectedWith(Error); + chai + .expect(deleteDialect(nonmember.program, dialect)) + .to.eventually.be.rejectedWith(Error); + }); + + it('Owner deletes the dialect', async () => { + const dialect = await createDialect(owner.program, members); + await deleteDialect(owner.program, dialect); + chai + .expect(getDialectForMembers(owner.program, members)) + .to.eventually.be.rejectedWith(Error); + }); + + it('Fail to subscribe a user twice to the same dialect (silent, noop)', async () => { + const dialect = await createDialect(owner.program, members); + await subscribeUser(owner.program, dialect, writer.publicKey); + const metadata = await getMetadata(program, writer.publicKey); + // subscribed once + chai + .expect( + metadata.subscriptions.filter((s) => + s.pubkey.equals(dialect.publicKey), + ).length, + ) + .to.equal(1); + chai + .expect(subscribeUser(owner.program, dialect, writer.publicKey)) + .to.be.rejectedWith(Error); + // still subscribed just once + chai + .expect( + metadata.subscriptions.filter((s) => + s.pubkey.equals(dialect.publicKey), + ).length, + ) + .to.equal(1); + }); + }); // describe('Find dialects', () => { // it('Can find all dialects filtering by user public key', async () => { @@ -1060,13 +1049,6 @@ describe('Protocol v1 test', () => { // }); // }); - interface User { - program: Program; - wallet: Wallet; - publicKey: PublicKey; - encryptionProps: EncryptionProps; - } - async function createUser( { requestAirdrop, createMeta }: CreateUserOptions = { requestAirdrop: true, @@ -1096,7 +1078,7 @@ describe('Protocol v1 test', () => { publicKey, 10 * web3.LAMPORTS_PER_SOL, ); - await connection.confirmTransaction(airDropRequest); + await connection.confirmTransaction(airDropRequest); // TODO: replace conneciton } if (createMeta) { await createMetadata(program); @@ -1127,11 +1109,17 @@ function generateRandomText(length: number) { return result; } +interface User { + program: Program; + wallet: Wallet; + publicKey: PublicKey; + encryptionProps: EncryptionProps; +} + async function createDialectAndSubscribeAllMembers( - program: Program, - owner: anchor.web3.Keypair, - member: anchor.web3.Keypair, - encrypted: boolean, + owner: User, + otherMember: PublicKey, + encrypted: boolean = false, ) { const members: Member[] = [ { @@ -1139,12 +1127,13 @@ async function createDialectAndSubscribeAllMembers( scopes: [true, true], // owner, read-only }, { - publicKey: member.publicKey, + publicKey: otherMember, scopes: [false, true], // non-owner, read-write }, ]; - const dialect = await createDialect(program, owner, members, encrypted); - await subscribeUser(program, dialect, owner.publicKey, owner); - await subscribeUser(program, dialect, member.publicKey, member); + const ownerProgram = owner.program; + const dialect = await createDialect(ownerProgram, members, encrypted); + await subscribeUser(ownerProgram, dialect, owner.publicKey); + await subscribeUser(ownerProgram, dialect, otherMember); return dialect; } From 43cca9c14f20f0107c7c1a37f933f97b2fbe77d6 Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Mon, 11 Apr 2022 17:58:31 +0400 Subject: [PATCH 10/10] Tests pass --- tests/test-v1.ts | 1449 +++++++++++++++++++++++----------------------- 1 file changed, 720 insertions(+), 729 deletions(-) diff --git a/tests/test-v1.ts b/tests/test-v1.ts index ec5b7c0..69e635b 100644 --- a/tests/test-v1.ts +++ b/tests/test-v1.ts @@ -9,19 +9,30 @@ import { createMetadata, deleteDialect, deleteMetadata, + DialectAccount, + Event, + findDialects, + getDialect, getDialectForMembers, getDialectProgramAddress, getDialects, getMetadata, Member, sendMessage, + subscribeToEvents, subscribeUser, } from '../src/api'; import { idl, programs, sleep, Wallet_ } from '../src/utils'; -import { ed25519KeyPairToCurve25519 } from '../src/utils/ecdh-encryption'; +import { + ed25519KeyPairToCurve25519, + ENCRYPTION_OVERHEAD_BYTES, +} from '../src/utils/ecdh-encryption'; import { Wallet } from '../src/utils/Wallet'; import { EncryptionProps } from '../src/api/text-serde'; import process from 'process'; +import { ITEM_METADATA_OVERHEAD } from '../src/utils/cyclic-bytebuffer'; +import { NONCE_SIZE_BYTES } from '../src/utils/nonce-generator'; +import { CountDownLatch } from '../src/utils/countdown-latch'; process.env.ANCHOR_WALLET = '/Users/tsmbl/.config/solana/id.json'; chai.use(chaiAsPromised); @@ -156,6 +167,7 @@ describe('Protocol v1 test', () => { chai .expect( writer.program.rpc.createDialect( + // TODO: deprecated / invalid # of args new anchor.BN(nonce), members.map((m) => m.scopes), { @@ -217,29 +229,29 @@ describe('Protocol v1 test', () => { ); }); - // it('Subscribe users to dialect', async () => { - // const dialect = await createDialect(owner.program, members); - // // owner subscribes themselves - // await subscribeUser(program, dialect, owner.publicKey, owner); - // // owner subscribes writer - // await subscribeUser(program, dialect, writer.publicKey, owner); - // const ownerMeta = await getMetadata(program, owner.publicKey); - // const writerMeta = await getMetadata(program, writer.publicKey); - // chai - // .expect( - // ownerMeta.subscriptions.filter((s) => - // s.pubkey.equals(dialect.publicKey), - // ).length, - // ) - // .to.equal(1); - // chai - // .expect( - // writerMeta.subscriptions.filter((s) => - // s.pubkey.equals(dialect.publicKey), - // ).length, - // ) - // .to.equal(1); - // }); + it('Subscribe users to dialect', async () => { + const dialect = await createDialect(owner.program, members); + // owner subscribes themselves + await subscribeUser(owner.program, dialect, owner.publicKey); + // owner subscribes writer + await subscribeUser(owner.program, dialect, writer.publicKey); + const ownerMeta = await getMetadata(owner.program, owner.publicKey); + const writerMeta = await getMetadata(owner.program, writer.publicKey); + chai + .expect( + ownerMeta.subscriptions.filter((s) => + s.pubkey.equals(dialect.publicKey), + ).length, + ) + .to.equal(1); + chai + .expect( + writerMeta.subscriptions.filter((s) => + s.pubkey.equals(dialect.publicKey), + ).length, + ) + .to.equal(1); + }); it('Should return list of dialects sorted by time desc', async () => { // given @@ -342,712 +354,688 @@ describe('Protocol v1 test', () => { }); }); - // describe('Find dialects', () => { - // it('Can find all dialects filtering by user public key', async () => { - // // given - // const [user1, user2, user3] = await Promise.all([ - // createUser({ - // requestAirdrop: true, - // createMeta: false, - // }).then((it) => it.user), - // createUser({ - // requestAirdrop: true, - // createMeta: false, - // }).then((it) => it.user), - // createUser({ - // requestAirdrop: true, - // createMeta: false, - // }).then((it) => it.user), - // ]); - // const [user1User2Dialect, user1User3Dialect, user2User3Dialect] = - // await Promise.all([ - // createDialect(program, user1, [ - // { - // publicKey: user1.publicKey, - // scopes: [true, true], - // }, - // { - // publicKey: user2.publicKey, - // scopes: [false, true], - // }, - // ]), - // createDialect(program, user1, [ - // { - // publicKey: user1.publicKey, - // scopes: [true, true], - // }, - // { - // publicKey: user3.publicKey, - // scopes: [false, true], - // }, - // ]), - // createDialect(program, user2, [ - // { - // publicKey: user2.publicKey, - // scopes: [true, true], - // }, - // { - // publicKey: user3.publicKey, - // scopes: [false, true], - // }, - // ]), - // ]); - // // when - // const [ - // user1Dialects, - // user2Dialects, - // user3Dialects, - // nonExistingUserDialects, - // ] = await Promise.all([ - // findDialects(program, { - // userPk: user1.publicKey, - // }), - // findDialects(program, { - // userPk: user2.publicKey, - // }), - // findDialects(program, { - // userPk: user3.publicKey, - // }), - // findDialects(program, { - // userPk: anchor.web3.Keypair.generate().publicKey, - // }), - // ]); - // // then - // expect( - // user1Dialects.map((it) => it.publicKey), - // ).to.deep.contain.all.members([ - // user1User2Dialect.publicKey, - // user1User3Dialect.publicKey, - // ]); - // expect( - // user2Dialects.map((it) => it.publicKey), - // ).to.deep.contain.all.members([ - // user1User2Dialect.publicKey, - // user2User3Dialect.publicKey, - // ]); - // expect( - // user3Dialects.map((it) => it.publicKey), - // ).to.deep.contain.all.members([ - // user2User3Dialect.publicKey, - // user1User3Dialect.publicKey, - // ]); - // expect(nonExistingUserDialects.length).to.be.eq(0); - // }); - // }); + describe('Find dialects', () => { + it('Can find all dialects filtering by user public key', async () => { + // given + const [user1, user2, user3] = await Promise.all([ + createUser({ + requestAirdrop: true, + createMeta: false, + }), + createUser({ + requestAirdrop: true, + createMeta: false, + }), + createUser({ + requestAirdrop: true, + createMeta: false, + }), + ]); + const [user1User2Dialect, user1User3Dialect, user2User3Dialect] = + await Promise.all([ + createDialect(user1.program, [ + { + publicKey: user1.publicKey, + scopes: [true, true], + }, + { + publicKey: user2.publicKey, + scopes: [false, true], + }, + ]), + createDialect(user1.program, [ + { + publicKey: user1.publicKey, + scopes: [true, true], + }, + { + publicKey: user3.publicKey, + scopes: [false, true], + }, + ]), + createDialect(user2.program, [ + { + publicKey: user2.publicKey, + scopes: [true, true], + }, + { + publicKey: user3.publicKey, + scopes: [false, true], + }, + ]), + ]); + // when + const [ + user1Dialects, + user2Dialects, + user3Dialects, + nonExistingUserDialects, + ] = await Promise.all([ + findDialects(program, { + userPk: user1.publicKey, + }), + findDialects(program, { + userPk: user2.publicKey, + }), + findDialects(program, { + userPk: user3.publicKey, + }), + findDialects(program, { + userPk: anchor.web3.Keypair.generate().publicKey, + }), + ]); + // then + expect( + user1Dialects.map((it) => it.publicKey), + ).to.deep.contain.all.members([ + user1User2Dialect.publicKey, + user1User3Dialect.publicKey, + ]); + expect( + user2Dialects.map((it) => it.publicKey), + ).to.deep.contain.all.members([ + user1User2Dialect.publicKey, + user2User3Dialect.publicKey, + ]); + expect( + user3Dialects.map((it) => it.publicKey), + ).to.deep.contain.all.members([ + user2User3Dialect.publicKey, + user1User3Dialect.publicKey, + ]); + expect(nonExistingUserDialects.length).to.be.eq(0); + }); + }); - // describe('Unencrypted messaging tests', () => { - // let owner: web3.Keypair; - // let writer: web3.Keypair; - // let nonmember: web3.Keypair; - // let members: Member[] = []; - // let dialect: DialectAccount; - // - // beforeEach(async () => { - // (owner = await createUser({ - // requestAirdrop: true, - // createMeta: true, - // }).then((it) => it.user)), - // (writer = await createUser({ - // requestAirdrop: true, - // createMeta: true, - // }).then((it) => it.user)), - // (nonmember = await createUser({ - // requestAirdrop: true, - // createMeta: false, - // }).then((it) => it.user)), - // (members = [ - // { - // publicKey: owner.publicKey, - // scopes: [true, false], // owner, read-only - // }, - // { - // publicKey: writer.publicKey, - // scopes: [false, true], // non-owner, read-write - // }, - // ]); - // dialect = await createDialect(program, owner, members, false); - // }); - // - // it('Message sender and receiver can read the message text and time', async () => { - // // given - // const dialect = await getDialectForMembers(program, members); - // const text = generateRandomText(256); - // // when - // await sendMessage(program, dialect, writer, text); - // // then - // const senderDialect = await getDialectForMembers( - // program, - // dialect.dialect.members, - // ); - // const message = senderDialect.dialect.messages[0]; - // chai.expect(message.text).to.be.eq(text); - // chai - // .expect(senderDialect.dialect.lastMessageTimestamp) - // .to.be.eq(message.timestamp); - // }); - // - // it('Anonymous user can read any of the messages', async () => { - // // given - // const senderDialect = await getDialectForMembers(program, members); - // const text = generateRandomText(256); - // await sendMessage(program, senderDialect, writer, text); - // // when / then - // const nonMemberDialect = await getDialectForMembers( - // program, - // dialect.dialect.members, - // ); - // const message = nonMemberDialect.dialect.messages[0]; - // chai.expect(message.text).to.be.eq(text); - // chai.expect(message.owner).to.be.deep.eq(writer.publicKey); - // chai - // .expect(nonMemberDialect.dialect.lastMessageTimestamp) - // .to.be.eq(message.timestamp); - // }); - // - // it('New messages overwrite old, retrieved messages are in order.', async () => { - // // emulate ideal message alignment withing buffer - // const rawBufferSize = 8192; - // const messagesPerDialect = 16; - // const numMessages = messagesPerDialect * 2; - // const salt = 3; - // const targetRawMessageSize = rawBufferSize / messagesPerDialect - salt; - // const timestampSize = 4; - // const ownerMemberIdxSize = 1; - // const messageSerializationOverhead = - // ITEM_METADATA_OVERHEAD + timestampSize + ownerMemberIdxSize; - // const targetTextSize = - // targetRawMessageSize - messageSerializationOverhead; - // const texts = Array(numMessages) - // .fill(0) - // .map(() => generateRandomText(targetTextSize)); - // for (let messageIdx = 0; messageIdx < numMessages; messageIdx++) { - // // verify last last N messages look correct - // const messageCounter = messageIdx + 1; - // const text = texts[messageIdx]; - // const dialect = await getDialectForMembers(program, members); - // console.log( - // `Sending message ${messageCounter}/${texts.length} - // len = ${text.length} - // idx: ${dialect.dialect.nextMessageIdx}`, - // ); - // await sendMessage(program, dialect, writer, text); - // const sliceStart = - // messageCounter <= messagesPerDialect - // ? 0 - // : messageCounter - messagesPerDialect; - // const expectedMessagesCount = Math.min( - // messageCounter, - // messagesPerDialect, - // ); - // const sliceEnd = sliceStart + expectedMessagesCount; - // const expectedMessages = texts.slice(sliceStart, sliceEnd).reverse(); - // const d = await getDialect(program, dialect.publicKey); - // const actualMessages = d.dialect.messages.map((m) => m.text); - // console.log(` msgs count after send: ${actualMessages.length}\n`); - // expect(actualMessages).to.be.deep.eq(expectedMessages); - // } - // }); - // - // it('Message text limit of 853 bytes can be sent/received', async () => { - // const maxMessageSizeBytes = 853; - // const texts = Array(30) - // .fill(0) - // .map(() => generateRandomText(maxMessageSizeBytes)); - // for (let messageIdx = 0; messageIdx < texts.length; messageIdx++) { - // const text = texts[messageIdx]; - // const messageCounter = messageIdx + 1; - // const dialect = await getDialectForMembers(program, members); - // console.log( - // `Sending message ${messageCounter}/${texts.length} - // len = ${text.length} - // idx: ${dialect.dialect.nextMessageIdx}`, - // ); - // // when - // await sendMessage(program, dialect, writer, text); - // const d = await getDialect(program, dialect.publicKey); - // const actualMessages = d.dialect.messages; - // const lastMessage = actualMessages[0]; - // console.log(` msgs count after send: ${actualMessages.length}\n`); - // // then - // expect(lastMessage.text).to.be.deep.eq(text); - // } - // }); - // }); + describe('Unencrypted messaging tests', () => { + let owner: User; + let writer: User; + let nonmember: User; + let members: Member[] = []; + let dialect: DialectAccount; - // describe('Encrypted messaging tests', () => { - // let owner: web3.Keypair; - // let ownerEncryptionProps: EncryptionProps; - // let writer: web3.Keypair; - // let writerEncryptionProps: EncryptionProps; - // let nonmember: web3.Keypair; - // let nonmemberEncryptionProps: EncryptionProps; - // let members: Member[] = []; - // let dialect: DialectAccount; - // - // beforeEach(async () => { - // const ownerUser = await createUser({ - // requestAirdrop: true, - // createMeta: true, - // }); - // owner = ownerUser.user; - // ownerEncryptionProps = ownerUser.encryptionProps; - // const writerUser = await createUser({ - // requestAirdrop: true, - // createMeta: true, - // }); - // writer = writerUser.user; - // writerEncryptionProps = writerUser.encryptionProps; - // const nonmemberUser = await createUser({ - // requestAirdrop: true, - // createMeta: false, - // }); - // nonmember = nonmemberUser.user; - // nonmemberEncryptionProps = nonmemberUser.encryptionProps; - // members = [ - // { - // publicKey: owner.publicKey, - // scopes: [true, false], // owner, read-only - // }, - // { - // publicKey: writer.publicKey, - // scopes: [false, true], // non-owner, read-write - // }, - // ]; - // dialect = await createDialect(program, owner, members, true); - // }); - // - // it('Message sender can send msg and then read the message text and time', async () => { - // // given - // const dialect = await getDialectForMembers( - // program, - // members, - // writerEncryptionProps, - // ); - // const text = generateRandomText(256); - // // when - // await sendMessage(program, dialect, writer, text, writerEncryptionProps); - // // then - // const senderDialect = await getDialectForMembers( - // program, - // dialect.dialect.members, - // writerEncryptionProps, - // ); - // const message = senderDialect.dialect.messages[0]; - // chai.expect(message.text).to.be.eq(text); - // chai.expect(message.owner).to.be.deep.eq(writer.publicKey); - // chai - // .expect(senderDialect.dialect.lastMessageTimestamp) - // .to.be.eq(message.timestamp); - // }); - // - // it('Message receiver can read the message text and time sent by sender', async () => { - // // given - // const senderDialect = await getDialectForMembers( - // program, - // members, - // writerEncryptionProps, - // ); - // const text = generateRandomText(256); - // // when - // await sendMessage( - // program, - // senderDialect, - // writer, - // text, - // writerEncryptionProps, - // ); - // // then - // const receiverDialect = await getDialectForMembers( - // program, - // dialect.dialect.members, - // ownerEncryptionProps, - // ); - // const message = receiverDialect.dialect.messages[0]; - // chai.expect(message.text).to.be.eq(text); - // chai.expect(message.owner).to.be.deep.eq(writer.publicKey); - // chai - // .expect(receiverDialect.dialect.lastMessageTimestamp) - // .to.be.eq(message.timestamp); - // }); - // - // it("Non-member can't read (decrypt) any of the messages", async () => { - // // given - // const senderDialect = await getDialectForMembers( - // program, - // members, - // writerEncryptionProps, - // ); - // const text = generateRandomText(256); - // await sendMessage( - // program, - // senderDialect, - // writer, - // text, - // writerEncryptionProps, - // ); - // // when / then - // expect( - // getDialectForMembers( - // program, - // dialect.dialect.members, - // nonmemberEncryptionProps, - // ), - // ).to.eventually.be.rejected; - // }); - // - // it('New messages overwrite old, retrieved messages are in order.', async () => { - // // emulate ideal message alignment withing buffer - // const rawBufferSize = 8192; - // const messagesPerDialect = 16; - // const numMessages = messagesPerDialect * 2; - // const salt = 3; - // const targetRawMessageSize = rawBufferSize / messagesPerDialect - salt; - // const timestampSize = 4; - // const ownerMemberIdxSize = 1; - // const messageSerializationOverhead = - // ITEM_METADATA_OVERHEAD + - // ENCRYPTION_OVERHEAD_BYTES + - // NONCE_SIZE_BYTES + - // timestampSize + - // ownerMemberIdxSize; - // const targetTextSize = - // targetRawMessageSize - messageSerializationOverhead; - // const texts = Array(numMessages) - // .fill(0) - // .map(() => generateRandomText(targetTextSize)); - // for (let messageIdx = 0; messageIdx < numMessages; messageIdx++) { - // // verify last last N messages look correct - // const messageCounter = messageIdx + 1; - // const text = texts[messageIdx]; - // const dialect = await getDialectForMembers( - // program, - // members, - // writerEncryptionProps, - // ); - // console.log( - // `Sending message ${messageCounter}/${texts.length} - // len = ${text.length} - // idx: ${dialect.dialect.nextMessageIdx}`, - // ); - // await sendMessage( - // program, - // dialect, - // writer, - // text, - // writerEncryptionProps, - // ); - // const sliceStart = - // messageCounter <= messagesPerDialect - // ? 0 - // : messageCounter - messagesPerDialect; - // const expectedMessagesCount = Math.min( - // messageCounter, - // messagesPerDialect, - // ); - // const sliceEnd = sliceStart + expectedMessagesCount; - // const expectedMessages = texts.slice(sliceStart, sliceEnd).reverse(); - // const d = await getDialect( - // program, - // dialect.publicKey, - // writerEncryptionProps, - // ); - // const actualMessages = d.dialect.messages.map((m) => m.text); - // console.log(` msgs count after send: ${actualMessages.length}\n`); - // expect(actualMessages).to.be.deep.eq(expectedMessages); - // } - // }); - // - // it('Send/receive random size messages.', async () => { - // const texts = Array(32) - // .fill(0) - // .map(() => generateRandomText(randomInt(256, 512))); - // for (let messageIdx = 0; messageIdx < texts.length; messageIdx++) { - // const text = texts[messageIdx]; - // const messageCounter = messageIdx + 1; - // const dialect = await getDialectForMembers( - // program, - // members, - // writerEncryptionProps, - // ); - // console.log( - // `Sending message ${messageCounter}/${texts.length} - // len = ${text.length} - // idx: ${dialect.dialect.nextMessageIdx}`, - // ); - // // when - // await sendMessage( - // program, - // dialect, - // writer, - // text, - // writerEncryptionProps, - // ); - // const d = await getDialect( - // program, - // dialect.publicKey, - // writerEncryptionProps, - // ); - // const actualMessages = d.dialect.messages; - // const lastMessage = actualMessages[0]; - // console.log(` msgs count after send: ${actualMessages.length}\n`); - // // then - // expect(lastMessage.text).to.be.deep.eq(text); - // } - // }); - // - // /* UTF-8 encoding summary: - // - ASCII characters are encoded using 1 byte - // - Roman, Greek, Cyrillic, Coptic, Armenian, Hebrew, Arabic characters are encoded using 2 bytes - // - Chinese and Japanese among others are encoded using 3 bytes - // - Emoji are encoded using 4 bytes - // A note about message length limit and summary: - // - len >= 814 hits max transaction size limit = 1232 bytes https://docs.solana.com/ru/proposals/transactions-v2 - // - => best case: 813 symbols per msg (ascii only) - // - => worst case: 203 symbols (e.g. emoji only) - // - => average case depends on character set, see details below: - // ---- ASCII: ±800 characters - // ---- Roman, Greek, Cyrillic, Coptic, Armenian, Hebrew, Arabic: ± 406 characters - // ---- Chinese and japanese: ± 270 characters - // ---- Emoji: ± 203 characters*/ - // it('Message text limit of 813 bytes can be sent/received', async () => { - // const maxMessageSizeBytes = 813; - // const texts = Array(30) - // .fill(0) - // .map(() => generateRandomText(maxMessageSizeBytes)); - // for (let messageIdx = 0; messageIdx < texts.length; messageIdx++) { - // const text = texts[messageIdx]; - // const messageCounter = messageIdx + 1; - // const dialect = await getDialectForMembers( - // program, - // members, - // writerEncryptionProps, - // ); - // console.log( - // `Sending message ${messageCounter}/${texts.length} - // len = ${text.length} - // idx: ${dialect.dialect.nextMessageIdx}`, - // ); - // // when - // await sendMessage( - // program, - // dialect, - // writer, - // text, - // writerEncryptionProps, - // ); - // const d = await getDialect( - // program, - // dialect.publicKey, - // writerEncryptionProps, - // ); - // const actualMessages = d.dialect.messages; - // const lastMessage = actualMessages[0]; - // console.log(` msgs count after send: ${actualMessages.length}\n`); - // // then - // expect(lastMessage.text).to.be.deep.eq(text); - // } - // }); - // - // it('2 writers can send a messages and read them when dialect state is linearized before sending msg', async () => { - // // given - // const writer1 = await createUser({ - // requestAirdrop: true, - // createMeta: true, - // }); - // const writer2 = await createUser({ - // requestAirdrop: true, - // createMeta: true, - // }); - // members = [ - // { - // publicKey: writer1.user.publicKey, - // scopes: [true, true], // owner, read-only - // }, - // { - // publicKey: writer2.user.publicKey, - // scopes: [false, true], // non-owner, read-write - // }, - // ]; - // await createDialect(program, writer1.user, members, true); - // // when - // let writer1Dialect = await getDialectForMembers( - // program, - // members, - // writer1.encryptionProps, - // ); - // const writer1Text = generateRandomText(256); - // await sendMessage( - // program, - // writer1Dialect, - // writer1.user, - // writer1Text, - // writer1.encryptionProps, - // ); - // let writer2Dialect = await getDialectForMembers( - // program, - // members, - // writer2.encryptionProps, - // ); // ensures dialect state linearization - // const writer2Text = generateRandomText(256); - // await sendMessage( - // program, - // writer2Dialect, - // writer2.user, - // writer2Text, - // writer2.encryptionProps, - // ); - // - // writer1Dialect = await getDialectForMembers( - // program, - // members, - // writer1.encryptionProps, - // ); - // writer2Dialect = await getDialectForMembers( - // program, - // members, - // writer2.encryptionProps, - // ); - // - // // then check writer1 dialect state - // const message1Writer1 = writer1Dialect.dialect.messages[1]; - // const message2Writer1 = writer1Dialect.dialect.messages[0]; - // chai.expect(message1Writer1.text).to.be.eq(writer1Text); - // chai.expect(message1Writer1.owner).to.be.deep.eq(writer1.user.publicKey); - // chai.expect(message2Writer1.text).to.be.eq(writer2Text); - // chai.expect(message2Writer1.owner).to.be.deep.eq(writer2.user.publicKey); - // // then check writer2 dialect state - // const message1Writer2 = writer2Dialect.dialect.messages[1]; - // const message2Writer2 = writer2Dialect.dialect.messages[0]; - // chai.expect(message1Writer2.text).to.be.eq(writer1Text); - // chai.expect(message1Writer2.owner).to.be.deep.eq(writer1.user.publicKey); - // chai.expect(message2Writer2.text).to.be.eq(writer2Text); - // chai.expect(message2Writer2.owner).to.be.deep.eq(writer2.user.publicKey); - // }); - // - // // This test was failing before changing nonce generation algorithm - // it('2 writers can send a messages and read them when dialect state is not linearized before sending msg', async () => { - // // given - // const writer1 = await createUser({ - // requestAirdrop: true, - // createMeta: true, - // }); - // const writer2 = await createUser({ - // requestAirdrop: true, - // createMeta: true, - // }); - // members = [ - // { - // publicKey: writer1.user.publicKey, - // scopes: [true, true], // owner, read-only - // }, - // { - // publicKey: writer2.user.publicKey, - // scopes: [false, true], // non-owner, read-write - // }, - // ]; - // await createDialect(program, writer1.user, members, true); - // // when - // let writer1Dialect = await getDialectForMembers( - // program, - // members, - // writer1.encryptionProps, - // ); - // let writer2Dialect = await getDialectForMembers( - // program, - // members, - // writer2.encryptionProps, - // ); // ensures no dialect state linearization - // const writer1Text = generateRandomText(256); - // await sendMessage( - // program, - // writer1Dialect, - // writer1.user, - // writer1Text, - // writer1.encryptionProps, - // ); - // const writer2Text = generateRandomText(256); - // await sendMessage( - // program, - // writer2Dialect, - // writer2.user, - // writer2Text, - // writer2.encryptionProps, - // ); - // - // writer1Dialect = await getDialectForMembers( - // program, - // members, - // writer1.encryptionProps, - // ); - // writer2Dialect = await getDialectForMembers( - // program, - // members, - // writer2.encryptionProps, - // ); - // - // // then check writer1 dialect state - // const message1Writer1 = writer1Dialect.dialect.messages[1]; - // const message2Writer1 = writer1Dialect.dialect.messages[0]; - // chai.expect(message1Writer1.text).to.be.eq(writer1Text); - // chai.expect(message1Writer1.owner).to.be.deep.eq(writer1.user.publicKey); - // chai.expect(message2Writer1.text).to.be.eq(writer2Text); - // chai.expect(message2Writer1.owner).to.be.deep.eq(writer2.user.publicKey); - // // then check writer2 dialect state - // const message1Writer2 = writer2Dialect.dialect.messages[1]; - // const message2Writer2 = writer2Dialect.dialect.messages[0]; - // chai.expect(message1Writer2.text).to.be.eq(writer1Text); - // chai.expect(message1Writer2.owner).to.be.deep.eq(writer1.user.publicKey); - // chai.expect(message2Writer2.text).to.be.eq(writer2Text); - // chai.expect(message2Writer2.owner).to.be.deep.eq(writer2.user.publicKey); - // }); - // }); + beforeEach(async () => { + const [u1, u2, u3] = await Promise.all([ + createUser({ + requestAirdrop: true, + createMeta: true, + }), + createUser({ + requestAirdrop: true, + createMeta: true, + }), + createUser({ + requestAirdrop: true, + createMeta: true, + }), + ]); + owner = u1; + writer = u2; + nonmember = u3; + members = [ + { + publicKey: owner.publicKey, + scopes: [true, false], // owner, read-only + }, + { + publicKey: writer.publicKey, + scopes: [false, true], // non-owner, read-write + }, + ]; + dialect = await createDialect(owner.program, members); + }); + + it('Message sender can read the message text and time', async () => { + // given + const text = generateRandomText(256); + // when + await sendMessage(writer.program, dialect, text); + // then + const senderDialect = await getDialectForMembers( + writer.program, + dialect.dialect.members, + ); + const message = senderDialect.dialect.messages[0]; + chai.expect(message.text).to.be.eq(text); + chai + .expect(senderDialect.dialect.lastMessageTimestamp) + .to.be.eq(message.timestamp); + }); + + it('Message receiver can read the message text and time', async () => { + // given + const text = generateRandomText(256); + // when + await sendMessage(writer.program, dialect, text); + // then + const receiverDialect = await getDialectForMembers( + owner.program, + dialect.dialect.members, + ); + const message = receiverDialect.dialect.messages[0]; + chai.expect(message.text).to.be.eq(text); + chai + .expect(receiverDialect.dialect.lastMessageTimestamp) + .to.be.eq(message.timestamp); + }); + + it('Anonymous user can read any of the messages', async () => { + // given + const text = generateRandomText(256); + await sendMessage(writer.program, dialect, text); + // when / then + const nonMemberDialect = await getDialectForMembers( + nonmember.program, + dialect.dialect.members, + ); + const message = nonMemberDialect.dialect.messages[0]; + chai.expect(message.text).to.be.eq(text); + chai.expect(message.owner).to.be.deep.eq(writer.publicKey); + chai + .expect(nonMemberDialect.dialect.lastMessageTimestamp) + .to.be.eq(message.timestamp); + }); + + it('New messages overwrite old, retrieved messages are in order.', async () => { + // emulate ideal message alignment withing buffer + const rawBufferSize = 8192; + const messagesPerDialect = 16; + const numMessages = messagesPerDialect * 2; + const salt = 3; + const targetRawMessageSize = rawBufferSize / messagesPerDialect - salt; + const timestampSize = 4; + const ownerMemberIdxSize = 1; + const messageSerializationOverhead = + ITEM_METADATA_OVERHEAD + timestampSize + ownerMemberIdxSize; + const targetTextSize = + targetRawMessageSize - messageSerializationOverhead; + const texts = Array(numMessages) + .fill(0) + .map(() => generateRandomText(targetTextSize)); + for (let messageIdx = 0; messageIdx < numMessages; messageIdx++) { + // verify last last N messages look correct + const messageCounter = messageIdx + 1; + const text = texts[messageIdx]; + const dialect = await getDialectForMembers(writer.program, members); + console.log( + `Sending message ${messageCounter}/${texts.length} + len = ${text.length} + idx: ${dialect.dialect.nextMessageIdx}`, + ); + await sendMessage(writer.program, dialect, text); + const sliceStart = + messageCounter <= messagesPerDialect + ? 0 + : messageCounter - messagesPerDialect; + const expectedMessagesCount = Math.min( + messageCounter, + messagesPerDialect, + ); + const sliceEnd = sliceStart + expectedMessagesCount; + const expectedMessages = texts.slice(sliceStart, sliceEnd).reverse(); + const d = await getDialect(writer.program, dialect.publicKey); + const actualMessages = d.dialect.messages.map((m) => m.text); + console.log(` msgs count after send: ${actualMessages.length}\n`); + expect(actualMessages).to.be.deep.eq(expectedMessages); + } + }); + + it('Message text limit of 853 bytes can be sent/received', async () => { + const maxMessageSizeBytes = 853; + const texts = Array(30) + .fill(0) + .map(() => generateRandomText(maxMessageSizeBytes)); + for (let messageIdx = 0; messageIdx < texts.length; messageIdx++) { + const text = texts[messageIdx]; + const messageCounter = messageIdx + 1; + const dialect = await getDialectForMembers(writer.program, members); + console.log( + `Sending message ${messageCounter}/${texts.length} + len = ${text.length} + idx: ${dialect.dialect.nextMessageIdx}`, + ); + // when + await sendMessage(writer.program, dialect, text); + const d = await getDialect(writer.program, dialect.publicKey); + const actualMessages = d.dialect.messages; + const lastMessage = actualMessages[0]; + console.log(` msgs count after send: ${actualMessages.length}\n`); + // then + expect(lastMessage.text).to.be.deep.eq(text); + } + }); + }); - // describe('Subscription tests', () => { - // let owner: web3.Keypair; - // let writer: web3.Keypair; - // - // beforeEach(async () => { - // owner = await createUser({ - // requestAirdrop: true, - // createMeta: false, - // }).then((it) => it.user); - // writer = await createUser({ - // requestAirdrop: true, - // createMeta: false, - // }).then((it) => it.user); - // }); - // - // it('Can subscribe to events and receive them and unsubscribe', async () => { - // // given - // const eventsAccumulator: Event[] = []; - // const expectedEvents = 8; - // const countDownLatch = new CountDownLatch(expectedEvents); - // const subscription = await subscribeToEvents(program, async (it) => { - // console.log('event', it); - // countDownLatch.countDown(); - // return eventsAccumulator.push(it); - // }); - // // when - // await createMetadata(program, owner); // 1 event - // await createMetadata(program, writer); // 1 event - // const dialectAccount = await createDialectAndSubscribeAllMembers( - // program, - // owner, - // writer, - // false, - // ); // 3 events - // await deleteMetadata(program, owner); // 1 event - // await deleteMetadata(program, writer); // 1 event - // await deleteDialect(program, dialectAccount, owner); // 1 event - // await countDownLatch.await(5000); - // await subscription.unsubscribe(); - // // events below should be ignored - // await createMetadata(program, owner); - // await createMetadata(program, writer); - // // then - // chai.expect(eventsAccumulator.length).to.be.eq(expectedEvents); - // }); - // }); + describe('Encrypted messaging tests', () => { + let owner: User; + let writer: User; + let nonmember: User; + let members: Member[] = []; + let dialect: DialectAccount; + + beforeEach(async () => { + const [u1, u2, u3] = await Promise.all([ + createUser({ + requestAirdrop: true, + createMeta: true, + }), + createUser({ + requestAirdrop: true, + createMeta: true, + }), + createUser({ + requestAirdrop: true, + createMeta: true, + }), + ]); + owner = u1; + writer = u2; + nonmember = u3; + members = [ + { + publicKey: owner.publicKey, + scopes: [true, false], // owner, read-only + }, + { + publicKey: writer.publicKey, + scopes: [false, true], // non-owner, read-write + }, + ]; + dialect = await createDialect(owner.program, members, true); + }); + + it('Message sender can send msg and then read the message text and time', async () => { + // given + const text = generateRandomText(256); + // when + await sendMessage(writer.program, dialect, text, writer.encryptionProps); + // then + const senderDialect = await getDialectForMembers( + writer.program, + dialect.dialect.members, + writer.encryptionProps, + ); + const message = senderDialect.dialect.messages[0]; + chai.expect(message.text).to.be.eq(text); + chai.expect(message.owner).to.be.deep.eq(writer.publicKey); + chai + .expect(senderDialect.dialect.lastMessageTimestamp) + .to.be.eq(message.timestamp); + }); + + it('Message receiver can read the message text and time sent by sender', async () => { + // given + const text = generateRandomText(256); + // when + await sendMessage(writer.program, dialect, text, writer.encryptionProps); + // then + const receiverDialect = await getDialectForMembers( + owner.program, + dialect.dialect.members, + owner.encryptionProps, + ); + const message = receiverDialect.dialect.messages[0]; + chai.expect(message.text).to.be.eq(text); + chai.expect(message.owner).to.be.deep.eq(writer.publicKey); + chai + .expect(receiverDialect.dialect.lastMessageTimestamp) + .to.be.eq(message.timestamp); + }); + + it("Non-member can't read (decrypt) any of the messages", async () => { + // given + const text = generateRandomText(256); + await sendMessage(writer.program, dialect, text, writer.encryptionProps); + // when / then + expect( + getDialectForMembers( + nonmember.program, + dialect.dialect.members, + nonmember.encryptionProps, + ), + ).to.eventually.be.rejected; + }); + + it('New messages overwrite old, retrieved messages are in order.', async () => { + // emulate ideal message alignment withing buffer + const rawBufferSize = 8192; + const messagesPerDialect = 16; + const numMessages = messagesPerDialect * 2; + const salt = 3; + const targetRawMessageSize = rawBufferSize / messagesPerDialect - salt; + const timestampSize = 4; + const ownerMemberIdxSize = 1; + const messageSerializationOverhead = + ITEM_METADATA_OVERHEAD + + ENCRYPTION_OVERHEAD_BYTES + + NONCE_SIZE_BYTES + + timestampSize + + ownerMemberIdxSize; + const targetTextSize = + targetRawMessageSize - messageSerializationOverhead; + const texts = Array(numMessages) + .fill(0) + .map(() => generateRandomText(targetTextSize)); + for (let messageIdx = 0; messageIdx < numMessages; messageIdx++) { + // verify last last N messages look correct + const messageCounter = messageIdx + 1; + const text = texts[messageIdx]; + const dialect = await getDialectForMembers( + writer.program, + members, + writer.encryptionProps, + ); + console.log( + `Sending message ${messageCounter}/${texts.length} + len = ${text.length} + idx: ${dialect.dialect.nextMessageIdx}`, + ); + await sendMessage( + writer.program, + dialect, + text, + writer.encryptionProps, + ); + const sliceStart = + messageCounter <= messagesPerDialect + ? 0 + : messageCounter - messagesPerDialect; + const expectedMessagesCount = Math.min( + messageCounter, + messagesPerDialect, + ); + const sliceEnd = sliceStart + expectedMessagesCount; + const expectedMessages = texts.slice(sliceStart, sliceEnd).reverse(); + const d = await getDialect( + writer.program, + dialect.publicKey, + writer.encryptionProps, + ); + const actualMessages = d.dialect.messages.map((m) => m.text); + console.log(` msgs count after send: ${actualMessages.length}\n`); + expect(actualMessages).to.be.deep.eq(expectedMessages); + } + }); + + it('Send/receive random size messages.', async () => { + const texts = Array(32) + .fill(0) + .map(() => generateRandomText(randomInteger(256, 512))); + for (let messageIdx = 0; messageIdx < texts.length; messageIdx++) { + const text = texts[messageIdx]; + const messageCounter = messageIdx + 1; + const dialect = await getDialectForMembers( + writer.program, + members, + writer.encryptionProps, + ); + console.log( + `Sending message ${messageCounter}/${texts.length} + len = ${text.length} + idx: ${dialect.dialect.nextMessageIdx}`, + ); + // when + await sendMessage( + writer.program, + dialect, + text, + writer.encryptionProps, + ); + const d = await getDialect( + program, + dialect.publicKey, + writer.encryptionProps, + ); + const actualMessages = d.dialect.messages; + const lastMessage = actualMessages[0]; + console.log(` msgs count after send: ${actualMessages.length}\n`); + // then + expect(lastMessage.text).to.be.deep.eq(text); + } + }); + + /* UTF-8 encoding summary: + - ASCII characters are encoded using 1 byte + - Roman, Greek, Cyrillic, Coptic, Armenian, Hebrew, Arabic characters are encoded using 2 bytes + - Chinese and Japanese among others are encoded using 3 bytes + - Emoji are encoded using 4 bytes + A note about message length limit and summary: + - len >= 814 hits max transaction size limit = 1232 bytes https://docs.solana.com/ru/proposals/transactions-v2 + - => best case: 813 symbols per msg (ascii only) + - => worst case: 203 symbols (e.g. emoji only) + - => average case depends on character set, see details below: + ---- ASCII: ±800 characters + ---- Roman, Greek, Cyrillic, Coptic, Armenian, Hebrew, Arabic: ± 406 characters + ---- Chinese and japanese: ± 270 characters + ---- Emoji: ± 203 characters*/ + it('Message text limit of 813 bytes can be sent/received', async () => { + const maxMessageSizeBytes = 813; + const texts = Array(30) + .fill(0) + .map(() => generateRandomText(maxMessageSizeBytes)); + for (let messageIdx = 0; messageIdx < texts.length; messageIdx++) { + const text = texts[messageIdx]; + const messageCounter = messageIdx + 1; + const dialect = await getDialectForMembers( + writer.program, + members, + writer.encryptionProps, + ); + console.log( + `Sending message ${messageCounter}/${texts.length} + len = ${text.length} + idx: ${dialect.dialect.nextMessageIdx}`, + ); + // when + await sendMessage( + writer.program, + dialect, + text, + writer.encryptionProps, + ); + const d = await getDialect( + writer.program, + dialect.publicKey, + writer.encryptionProps, + ); + const actualMessages = d.dialect.messages; + const lastMessage = actualMessages[0]; + console.log(` msgs count after send: ${actualMessages.length}\n`); + // then + expect(lastMessage.text).to.be.deep.eq(text); + } + }); + + it('2 writers can send a messages and read them when dialect state is linearized before sending msg', async () => { + // given + const [writer1, writer2] = await Promise.all([ + createUser(), + createUser(), + ]); + members = [ + { + publicKey: writer1.publicKey, + scopes: [true, true], // owner, read-write + }, + { + publicKey: writer2.publicKey, + scopes: [false, true], // non-owner, read-write + }, + ]; + await createDialect(writer1.program, members, true); + // when + let writer1Dialect = await getDialectForMembers( + writer1.program, + members, + writer1.encryptionProps, + ); + const writer1Text = generateRandomText(256); + await sendMessage( + writer1.program, + writer1Dialect, + writer1Text, + writer1.encryptionProps, + ); + let writer2Dialect = await getDialectForMembers( + writer2.program, + members, + writer2.encryptionProps, + ); // ensures dialect state linearization + const writer2Text = generateRandomText(256); + await sendMessage( + writer2.program, + writer2Dialect, + writer2Text, + writer2.encryptionProps, + ); + + writer1Dialect = await getDialectForMembers( + writer1.program, + members, + writer1.encryptionProps, + ); + writer2Dialect = await getDialectForMembers( + writer2.program, + members, + writer2.encryptionProps, + ); + + // then check writer1 dialect state + const message1Writer1 = writer1Dialect.dialect.messages[1]; + const message2Writer1 = writer1Dialect.dialect.messages[0]; + chai.expect(message1Writer1.text).to.be.eq(writer1Text); + chai.expect(message1Writer1.owner).to.be.deep.eq(writer1.publicKey); + chai.expect(message2Writer1.text).to.be.eq(writer2Text); + chai.expect(message2Writer1.owner).to.be.deep.eq(writer2.publicKey); + // then check writer2 dialect state + const message1Writer2 = writer2Dialect.dialect.messages[1]; + const message2Writer2 = writer2Dialect.dialect.messages[0]; + chai.expect(message1Writer2.text).to.be.eq(writer1Text); + chai.expect(message1Writer2.owner).to.be.deep.eq(writer1.publicKey); + chai.expect(message2Writer2.text).to.be.eq(writer2Text); + chai.expect(message2Writer2.owner).to.be.deep.eq(writer2.publicKey); + }); + + // This test was failing before changing nonce generation algorithm + it('2 writers can send a messages and read them when dialect state is not linearized before sending msg', async () => { + // given + const [writer1, writer2] = await Promise.all([ + createUser(), + createUser(), + ]); + members = [ + { + publicKey: writer1.publicKey, + scopes: [true, true], // owner, read-only + }, + { + publicKey: writer2.publicKey, + scopes: [false, true], // non-owner, read-write + }, + ]; + await createDialect(writer1.program, members, true); + // when + let writer1Dialect = await getDialectForMembers( + program, + members, + writer1.encryptionProps, + ); + let writer2Dialect = await getDialectForMembers( + program, + members, + writer2.encryptionProps, + ); // ensures no dialect state linearization + const writer1Text = generateRandomText(256); + await sendMessage( + writer1.program, + writer1Dialect, + writer1Text, + writer1.encryptionProps, + ); + const writer2Text = generateRandomText(256); + await sendMessage( + writer2.program, + writer2Dialect, + writer2Text, + writer2.encryptionProps, + ); + + writer1Dialect = await getDialectForMembers( + program, + members, + writer1.encryptionProps, + ); + writer2Dialect = await getDialectForMembers( + program, + members, + writer2.encryptionProps, + ); + + // then check writer1 dialect state + const message1Writer1 = writer1Dialect.dialect.messages[1]; + const message2Writer1 = writer1Dialect.dialect.messages[0]; + chai.expect(message1Writer1.text).to.be.eq(writer1Text); + chai.expect(message1Writer1.owner).to.be.deep.eq(writer1.publicKey); + chai.expect(message2Writer1.text).to.be.eq(writer2Text); + chai.expect(message2Writer1.owner).to.be.deep.eq(writer2.publicKey); + // then check writer2 dialect state + const message1Writer2 = writer2Dialect.dialect.messages[1]; + const message2Writer2 = writer2Dialect.dialect.messages[0]; + chai.expect(message1Writer2.text).to.be.eq(writer1Text); + chai.expect(message1Writer2.owner).to.be.deep.eq(writer1.publicKey); + chai.expect(message2Writer2.text).to.be.eq(writer2Text); + chai.expect(message2Writer2.owner).to.be.deep.eq(writer2.publicKey); + }); + }); + + describe('Subscription tests', () => { + let owner: User; + let writer: User; + + beforeEach(async () => { + const [u1, u2] = await Promise.all([ + createUser({ + requestAirdrop: true, + createMeta: false, + }), + createUser({ + requestAirdrop: true, + createMeta: false, + }), + ]); + owner = u1; + writer = u2; + }); + + it('Can subscribe to events and receive them and unsubscribe', async () => { + // given + const eventsAccumulator: Event[] = []; + const expectedEvents = 8; + const countDownLatch = new CountDownLatch(expectedEvents); + const subscription = await subscribeToEvents(program, async (it) => { + console.log('event', it); + countDownLatch.countDown(); + return eventsAccumulator.push(it); + }); + // when + await createMetadata(writer.program); // 1 event + await createMetadata(owner.program); // 1 event + const dialectAccount = await createDialectAndSubscribeAllMembers( + owner, + writer.publicKey, + ); // 3 events + await deleteMetadata(owner.program); // 1 event + await deleteMetadata(writer.program); // 1 event + await deleteDialect(owner.program, dialectAccount); // 1 event + await countDownLatch.await(5000); + await subscription.unsubscribe(); + // events below should be ignored + await createMetadata(owner.program); + await createMetadata(writer.program); + // then + chai.expect(eventsAccumulator.length).to.be.eq(expectedEvents); + }); + }); async function createUser( { requestAirdrop, createMeta }: CreateUserOptions = { @@ -1064,7 +1052,6 @@ describe('Protocol v1 test', () => { wallet, Provider.defaultOptions(), ); - // @ts-ignore const NETWORK_NAME = 'localnet'; const DIALECT_PROGRAM_ADDRESS = programs[NETWORK_NAME].programAddress; const program = new Program( @@ -1119,7 +1106,7 @@ interface User { async function createDialectAndSubscribeAllMembers( owner: User, otherMember: PublicKey, - encrypted: boolean = false, + encrypted = false, ) { const members: Member[] = [ { @@ -1137,3 +1124,7 @@ async function createDialectAndSubscribeAllMembers( await subscribeUser(ownerProgram, dialect, otherMember); return dialect; } + +function randomInteger(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1)) + min; +}