diff --git a/programs/wordcel/src/error.rs b/programs/wordcel/src/error.rs index 7b8c043..5395811 100644 --- a/programs/wordcel/src/error.rs +++ b/programs/wordcel/src/error.rs @@ -9,3 +9,8 @@ pub enum PostError { pub enum ConnectionError { SelfFollow, } + +#[error_code] +pub enum AdminError { + UnAuthorizedAccess, +} diff --git a/programs/wordcel/src/instructions.rs b/programs/wordcel/src/instructions.rs index f168810..85c497f 100644 --- a/programs/wordcel/src/instructions.rs +++ b/programs/wordcel/src/instructions.rs @@ -1,6 +1,7 @@ use crate::*; use invite::program::Invite as InvitationProgram; use invite::Invite; +use std::str::FromStr; #[derive(Accounts)] #[instruction(random_hash: [u8;32])] @@ -32,6 +33,44 @@ pub struct Initialize<'info> { pub invitation_program: Program<'info, InvitationProgram>, } +#[derive(Accounts)] +pub struct TransferProfile<'info> { + #[account( + mut, + seeds = [ + b"profile".as_ref(), + &profile.random_hash + ], + bump, + )] + pub profile: Account<'info, Profile>, + #[account( + owner = invitation_program.key(), + seeds = [ + Invite::PREFIX.as_bytes().as_ref(), + authority.key().as_ref() + ], + seeds::program = invitation_program.key(), + bump = invitation.bump + )] + pub invitation: Account<'info, Invite>, + #[account( + owner = invitation_program.key(), + seeds = [ + Invite::PREFIX.as_bytes().as_ref(), + new_authority.key().as_ref() + ], + seeds::program = invitation_program.key(), + bump = new_authority_invitation.bump + )] + pub new_authority_invitation: Account<'info, Invite>, + #[account(mut)] + pub authority: Signer<'info>, + pub new_authority: SystemAccount<'info>, + pub system_program: Program<'info, System>, + pub invitation_program: Program<'info, InvitationProgram>, +} + #[derive(Accounts)] #[instruction(metadata_uri: String, random_hash: [u8;32])] pub struct CreatePost<'info> { @@ -162,3 +201,178 @@ pub struct CloseConnection<'info> { pub authority: Signer<'info>, pub system_program: Program<'info, System>, } + +#[derive(Accounts)] +#[instruction(random_hash: [u8; 32])] +pub struct InitializeConnectionBox<'info> { + #[account( + init, + seeds = [ + b"connection_box".as_ref(), + &random_hash + ], + bump, + payer = authority, + space = ConnectionBox::LEN + )] + pub connection_box: Account<'info, ConnectionBox>, + #[account(mut)] + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct TranferConnectionBox<'info> { + #[account( + mut, + seeds = [ + b"connection_box".as_ref(), + &connection_box.random_hash + ], + bump, + )] + pub connection_box: Account<'info, ConnectionBox>, + #[account(mut)] + pub authority: Signer<'info>, + pub new_authority: SystemAccount<'info>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct InitializeConnectionV2<'info> { + #[account( + init, + seeds = [ + b"connection_v2".as_ref(), + profile.key().as_ref() + ], + bump, + payer = authority, + // Don't allow the user to follow themselves + constraint = profile.authority.key() != connection_box.authority.key() @ConnectionError::SelfFollow, + space = ConnectionV2::LEN + )] + pub connection: Account<'info, ConnectionV2>, + #[account(has_one=authority)] + pub connection_box: Account<'info, ConnectionBox>, + pub profile: Account<'info, Profile>, + #[account(mut)] + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct CloseConnectionV2<'info> { + #[account( + mut, + seeds = [ + b"connection_v2".as_ref(), + profile.key().as_ref() + ], + bump = connection.bump, + has_one = connection_box, + close = authority + )] + pub connection: Account<'info, ConnectionV2>, + #[account(has_one=authority)] + pub connection_box: Account<'info, ConnectionBox>, + pub profile: Account<'info, Profile>, + #[account(mut)] + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct MigrateConnectionToV2<'info> { + #[account( + mut, + seeds = [ + b"connection".as_ref(), + authority.key().as_ref(), + profile.key().as_ref() + ], + bump = connection_v1.bump, + has_one = authority, + close = authority + )] + pub connection_v1: Account<'info, Connection>, + #[account( + init, + seeds = [ + b"connection_v2".as_ref(), + profile.key().as_ref() + ], + bump, + payer = authority, + // Don't allow the user to follow themselves + constraint = profile.authority.key() != connection_box.authority.key() @ConnectionError::SelfFollow, + space = ConnectionV2::LEN + )] + pub connection_v2: Account<'info, ConnectionV2>, + #[account(has_one=authority)] + pub connection_box: Account<'info, ConnectionBox>, + pub profile: Account<'info, Profile>, + #[account(mut)] + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, +} + +// NOTE: This instruction doesn't close the existing connection, but merely copies it over to +// connection v2. +// This is added so that the admin can easily migrate the conenctions if they chose to by paying +// for it and this helps us offer a better user experience. + +#[derive(Accounts)] +#[instruction(random_hash: [u8; 32])] +pub struct MigrateConnectionToV2Admin<'info> { + #[account( + seeds = [ + b"connection".as_ref(), + authority.key().as_ref(), + profile.key().as_ref() + ], + bump = connection_v1.bump, + has_one = authority, + )] + pub connection_v1: Account<'info, Connection>, + #[account( + init, + seeds = [ + b"connection_v2".as_ref(), + profile.key().as_ref() + ], + bump, + payer = payer, + // Don't allow the user to follow themselves + constraint = profile.authority.key() != authority.key() @ConnectionError::SelfFollow, + space = ConnectionV2::LEN + )] + pub connection_v2: Account<'info, ConnectionV2>, + #[account( + init, + seeds = [ + b"connection_box".as_ref(), + &random_hash + ], + bump, + payer = payer, + space = ConnectionBox::LEN + )] + pub connection_box: Account<'info, ConnectionBox>, + pub profile: Account<'info, Profile>, + pub authority: SystemAccount<'info>, + #[account(mut, constraint = is_admin(payer.key()) @AdminError::UnAuthorizedAccess)] + pub payer: Signer<'info>, + pub system_program: Program<'info, System>, +} + +fn is_admin(key: Pubkey) -> bool { + let admin_keys: Vec = [ + // Wordcel Admin + "8f2yAM5ufEC9WgHYdAxeDgpZqE1B1Q47CciPRZaDN3jc", + ] + .iter() + .map(|k| Pubkey::from_str(k).unwrap()) + .collect(); + admin_keys.contains(&key) +} diff --git a/programs/wordcel/src/lib.rs b/programs/wordcel/src/lib.rs index 1584ee9..bfb324b 100644 --- a/programs/wordcel/src/lib.rs +++ b/programs/wordcel/src/lib.rs @@ -31,6 +31,12 @@ pub mod wordcel { Ok(()) } + pub fn transfer_profile(ctx: Context) -> Result<()> { + let profile = &mut ctx.accounts.profile; + profile.authority = *ctx.accounts.new_authority.to_account_info().key; + Ok(()) + } + pub fn create_post( ctx: Context, metadata_uri: String, @@ -83,6 +89,7 @@ pub mod wordcel { Ok(()) } + // Marked for deprecation pub fn initialize_connection(ctx: Context) -> Result<()> { let connection = &mut ctx.accounts.connection; connection.bump = *ctx.bumps.get("connection").unwrap(); @@ -100,7 +107,63 @@ pub mod wordcel { Ok(()) } + // Marked for deprecation pub fn close_connection(_ctx: Context) -> Result<()> { Ok(()) } + + pub fn initialize_connection_v2(ctx: Context) -> Result<()> { + let connection = &mut ctx.accounts.connection; + connection.bump = *ctx.bumps.get("connection").unwrap(); + connection.profile = *ctx.accounts.profile.to_account_info().key; + connection.connection_box = *ctx.accounts.connection_box.to_account_info().key; + Ok(()) + } + + pub fn close_connection_v2(_ctx: Context) -> Result<()> { + Ok(()) + } + + pub fn initialize_connection_box( + ctx: Context, + random_hash: [u8; 32], + ) -> Result<()> { + let connection_box = &mut ctx.accounts.connection_box; + connection_box.bump = *ctx.bumps.get("connection_box").unwrap(); + connection_box.random_hash = random_hash; + connection_box.authority = *ctx.accounts.authority.to_account_info().key; + Ok(()) + } + + pub fn transfer_connection_box(ctx: Context) -> Result<()> { + let connection_box = &mut ctx.accounts.connection_box; + connection_box.authority = *ctx.accounts.new_authority.to_account_info().key; + Ok(()) + } + + pub fn migrate_to_connectionv2(ctx: Context) -> Result<()> { + let connection = &mut ctx.accounts.connection_v2; + connection.bump = *ctx.bumps.get("connection_v2").unwrap(); + connection.profile = *ctx.accounts.profile.to_account_info().key; + connection.connection_box = *ctx.accounts.connection_box.to_account_info().key; + Ok(()) + } + + pub fn migrate_to_connectionv2_admin( + ctx: Context, + random_hash: [u8; 32], + ) -> Result<()> { + // Set up connection box + let connection_box = &mut ctx.accounts.connection_box; + connection_box.bump = *ctx.bumps.get("connection_box").unwrap(); + connection_box.random_hash = random_hash; + connection_box.authority = *ctx.accounts.authority.to_account_info().key; + + // Set connection + let connection = &mut ctx.accounts.connection_v2; + connection.bump = *ctx.bumps.get("connection_v2").unwrap(); + connection.profile = *ctx.accounts.profile.to_account_info().key; + connection.connection_box = *ctx.accounts.connection_box.to_account_info().key; + Ok(()) + } } diff --git a/programs/wordcel/src/state.rs b/programs/wordcel/src/state.rs index 0eaa819..00c587f 100644 --- a/programs/wordcel/src/state.rs +++ b/programs/wordcel/src/state.rs @@ -41,3 +41,27 @@ pub struct Connection { impl Connection { pub const LEN: usize = 8 + size_of::(); } + +#[account] +#[derive(Default)] +pub struct ConnectionBox { + pub authority: Pubkey, + pub random_hash: [u8; 32], + pub bump: u8, +} + +impl ConnectionBox { + pub const LEN: usize = 8 + size_of::(); +} + +#[account] +#[derive(Default)] +pub struct ConnectionV2 { + pub profile: Pubkey, + pub connection_box: Pubkey, + pub bump: u8, +} + +impl ConnectionV2 { + pub const LEN: usize = 8 + size_of::(); +} diff --git a/tests/invite.spec.ts b/tests/invite.spec.ts index 9feaacd..1563d01 100644 --- a/tests/invite.spec.ts +++ b/tests/invite.spec.ts @@ -1,137 +1,159 @@ -import * as anchor from '@project-serum/anchor'; -import {Program, AnchorError} from '@project-serum/anchor'; -import {Invite} from '../target/types/invite'; -import {expect} from 'chai'; -import {PublicKey} from '@solana/web3.js'; -import {getInviteAccount, sendInvite} from "./utils/invite"; -import {airdrop} from './utils'; - -const {SystemProgram} = anchor.web3; +import * as anchor from "@project-serum/anchor"; +import { Program, AnchorError } from "@project-serum/anchor"; +import { Invite } from "../target/types/invite"; +import { expect } from "chai"; +import { PublicKey } from "@solana/web3.js"; +import { getInviteAccount, sendInvite } from "./utils/invite"; +import { airdrop } from "./utils"; + +const { SystemProgram } = anchor.web3; const provider = anchor.getProvider(); const program = anchor.workspace.Invite as Program; const user = provider.wallet.publicKey; - -describe('Invitation', async () => { - // Prepare test user. - const testUser = anchor.web3.Keypair.generate(); - let oneInviteAccount: PublicKey; - - before(async () => { - await airdrop(testUser.publicKey); - oneInviteAccount = await getInviteAccount(testUser.publicKey); - }); - - - it("should initialize", async () => { - - await program.methods.initialize() - .accounts({ - inviteAccount: oneInviteAccount, - authority: testUser.publicKey, - payer: user, - systemProgram: SystemProgram.programId - }) - .rpc(); - const data = await program.account.invite.fetch(oneInviteAccount); - expect(data.authority.toString()).to.equal(testUser.publicKey.toString()); - }); - - it("should send invite to others", async () => { - const randomUser = anchor.web3.Keypair.generate(); - const [inviter, invited] = await sendInvite(testUser, randomUser.publicKey, user) - const data = await program.account.invite.fetch(inviter); - expect(data.invitesLeft).to.equal(1); - expect(data.invitesSent).to.equal(1); - const toInviteData = await program.account.invite.fetch(invited); - expect(toInviteData.invitedBy.toString()).to.equal(testUser.publicKey.toString()); - expect(toInviteData.authority.toString()).to.equal(randomUser.publicKey.toString()); - }); - - it("should not be able to send more than 2 invites", async () => { - // Set up new user - const newUser = anchor.web3.Keypair.generate(); - await airdrop(newUser.publicKey); - const inviteAccount = await getInviteAccount(newUser.publicKey); - await program.methods.initialize() - .accounts({ - inviteAccount: inviteAccount, - authority: newUser.publicKey, - payer: user, - systemProgram: SystemProgram.programId - }) - .rpc(); - - //First Invite - const randomUser = anchor.web3.Keypair.generate(); - await sendInvite(newUser, randomUser.publicKey, user); - - //Second Invite - const randomUser1 = anchor.web3.Keypair.generate(); - await sendInvite(newUser, randomUser1.publicKey, user); - - // Third Invite - const randomUser2 = anchor.web3.Keypair.generate(); - try { - await sendInvite(newUser, randomUser2.publicKey, user); - } catch (error) { - const anchorError = AnchorError.parse(error.logs); - expect(anchorError.error.errorCode.code).to.equal('NoInvitesLeft'); - } - }); - - - it("should not allow random user to initialize", async () => { - const randomUser = anchor.web3.Keypair.generate(); - const seed = [Buffer.from("invite"), randomUser.publicKey.toBuffer()]; - const [account, _] = await anchor.web3.PublicKey.findProgramAddress(seed, program.programId); - const tx = await program.methods.initialize() - .accounts({ - inviteAccount: account, - authority: randomUser.publicKey, - payer: testUser.publicKey, - systemProgram: SystemProgram.programId - }) - .transaction(); - tx.feePayer = user; - tx.recentBlockhash = (await provider.connection.getRecentBlockhash()).blockhash; - tx.sign(testUser); - try { - await provider.sendAndConfirm(tx) - } catch (error) { - const anchorError = AnchorError.parse(error.logs); - expect(anchorError.error.errorCode.code).to.equal('UnAuthorizedInitialization'); - } - }); - - it("should allow admin to initialize as many accounts as required", async () => { - for (let index = 0; index < 5; index++) { - const randomUser = anchor.web3.Keypair.generate(); - const seed = [Buffer.from("invite"), randomUser.publicKey.toBuffer()]; - const [account, _] = await anchor.web3.PublicKey.findProgramAddress(seed, program.programId); - await program.methods.initialize() - .accounts({ - inviteAccount: account, - authority: randomUser.publicKey, - payer: user, - systemProgram: SystemProgram.programId - }) - .rpc(); - const data = await program.account.invite.fetch(account); - expect(data.authority.toString()).to.equal(randomUser.publicKey.toString()); - } - }); - - it("should not allow uninitialized invite account to send an invite", async () => { - const randomUser = anchor.web3.Keypair.generate(); - const randomUser1 = anchor.web3.Keypair.generate(); - await airdrop(randomUser1.publicKey); - try { - await sendInvite(randomUser, randomUser1.publicKey, user) - } catch (error) { - const anchorError = AnchorError.parse(error.logs); - expect(anchorError.error.errorCode.code).to.equal('AccountNotInitialized'); - } - }); +describe("Invitation", async () => { + // Prepare test user. + const testUser = anchor.web3.Keypair.generate(); + let oneInviteAccount: PublicKey; + + before(async () => { + await airdrop(testUser.publicKey); + oneInviteAccount = await getInviteAccount(testUser.publicKey); + }); + + it("should initialize", async () => { + await program.methods + .initialize() + .accounts({ + inviteAccount: oneInviteAccount, + authority: testUser.publicKey, + payer: user, + systemProgram: SystemProgram.programId, + }) + .rpc(); + const data = await program.account.invite.fetch(oneInviteAccount); + expect(data.authority.toString()).to.equal(testUser.publicKey.toString()); + }); + + it("should send invite to others", async () => { + const randomUser = anchor.web3.Keypair.generate(); + const [inviter, invited] = await sendInvite( + testUser, + randomUser.publicKey, + user + ); + const data = await program.account.invite.fetch(inviter); + expect(data.invitesLeft).to.equal(1); + expect(data.invitesSent).to.equal(1); + const toInviteData = await program.account.invite.fetch(invited); + expect(toInviteData.invitedBy.toString()).to.equal( + testUser.publicKey.toString() + ); + expect(toInviteData.authority.toString()).to.equal( + randomUser.publicKey.toString() + ); + }); + + it("should not be able to send more than 2 invites", async () => { + // Set up new user + const newUser = anchor.web3.Keypair.generate(); + await airdrop(newUser.publicKey); + const inviteAccount = await getInviteAccount(newUser.publicKey); + await program.methods + .initialize() + .accounts({ + inviteAccount: inviteAccount, + authority: newUser.publicKey, + payer: user, + systemProgram: SystemProgram.programId, + }) + .rpc(); + + //First Invite + const randomUser = anchor.web3.Keypair.generate(); + await sendInvite(newUser, randomUser.publicKey, user); + + //Second Invite + const randomUser1 = anchor.web3.Keypair.generate(); + await sendInvite(newUser, randomUser1.publicKey, user); + + // Third Invite + const randomUser2 = anchor.web3.Keypair.generate(); + try { + await sendInvite(newUser, randomUser2.publicKey, user); + } catch (error) { + const anchorError = AnchorError.parse(error.logs); + expect(anchorError.error.errorCode.code).to.equal("NoInvitesLeft"); + } + }); + + it("should not allow random user to initialize", async () => { + const randomUser = anchor.web3.Keypair.generate(); + const seed = [Buffer.from("invite"), randomUser.publicKey.toBuffer()]; + const [account, _] = await anchor.web3.PublicKey.findProgramAddress( + seed, + program.programId + ); + const tx = await program.methods + .initialize() + .accounts({ + inviteAccount: account, + authority: randomUser.publicKey, + payer: testUser.publicKey, + systemProgram: SystemProgram.programId, + }) + .transaction(); + tx.feePayer = user; + tx.recentBlockhash = ( + await provider.connection.getRecentBlockhash() + ).blockhash; + tx.sign(testUser); + try { + await provider.sendAndConfirm(tx); + } catch (error) { + const anchorError = AnchorError.parse(error.logs); + expect(anchorError.error.errorCode.code).to.equal( + "UnAuthorizedInitialization" + ); + } + }); + + it("should allow admin to initialize as many accounts as required", async () => { + for (let index = 0; index < 5; index++) { + const randomUser = anchor.web3.Keypair.generate(); + const seed = [Buffer.from("invite"), randomUser.publicKey.toBuffer()]; + const [account, _] = await anchor.web3.PublicKey.findProgramAddress( + seed, + program.programId + ); + await program.methods + .initialize() + .accounts({ + inviteAccount: account, + authority: randomUser.publicKey, + payer: user, + systemProgram: SystemProgram.programId, + }) + .rpc(); + const data = await program.account.invite.fetch(account); + expect(data.authority.toString()).to.equal( + randomUser.publicKey.toString() + ); + } + }); + + it("should not allow uninitialized invite account to send an invite", async () => { + const randomUser = anchor.web3.Keypair.generate(); + const randomUser1 = anchor.web3.Keypair.generate(); + await airdrop(randomUser1.publicKey); + try { + await sendInvite(randomUser, randomUser1.publicKey, user); + } catch (error) { + const anchorError = AnchorError.parse(error.logs); + expect(anchorError.error.errorCode.code).to.equal( + "AccountNotInitialized" + ); + } + }); }); diff --git a/tests/transfer.spec.ts b/tests/transfer.spec.ts new file mode 100644 index 0000000..fbf5a28 --- /dev/null +++ b/tests/transfer.spec.ts @@ -0,0 +1,261 @@ +import * as anchor from "@project-serum/anchor"; +import { Program, AnchorError } from "@project-serum/anchor"; +import { Wordcel } from "../target/types/wordcel"; +import { expect } from "chai"; +import { PublicKey } from "@solana/web3.js"; +import randombytes from "randombytes"; +import { getInviteAccount, invitationProgram } from "./utils/invite"; +import { airdrop } from "./utils"; +const { SystemProgram } = anchor.web3; +const provider = anchor.getProvider(); + +const program = anchor.workspace.Wordcel as Program; +const user = provider.wallet.publicKey; + +// NOTE: +// State Transtion happens as follows: +// New Invite -> New Profile -> New Post -> Identify new authority -> Send invite -> Transfer authority -> Old authority can't edit -> New authority can edit. +// How will the transfer work for connections? + +describe("Wordcel", async () => { + const originalAuthority = anchor.web3.Keypair.generate(); + const newAuthority = anchor.web3.Keypair.generate(); + + let originalAuthorityInvite: PublicKey; + let newAuthorityInvite: PublicKey; + + let lastPostHash: Buffer; + let lastPostAccount: PublicKey; + + let profileHash = randombytes(32); + const profileSeed = [Buffer.from("profile"), profileHash]; + const [profileAccount, _] = await anchor.web3.PublicKey.findProgramAddress( + profileSeed, + program.programId + ); + + before(async () => { + await airdrop(originalAuthority.publicKey); + await airdrop(newAuthority.publicKey); + + originalAuthorityInvite = await getInviteAccount( + originalAuthority.publicKey + ); + + await invitationProgram.methods + .initialize() + .accounts({ + inviteAccount: originalAuthorityInvite, + authority: originalAuthority.publicKey, + payer: user, + systemProgram: SystemProgram.programId, + }) + .rpc(); + + newAuthorityInvite = await getInviteAccount(newAuthority.publicKey); + await invitationProgram.methods + .initialize() + .accounts({ + inviteAccount: newAuthorityInvite, + authority: newAuthority.publicKey, + payer: user, + systemProgram: SystemProgram.programId, + }) + .rpc(); + + await program.methods + .initialize(profileHash) + .accounts({ + profile: profileAccount, + user: originalAuthority.publicKey, + invitation: originalAuthorityInvite, + invitationProgram: invitationProgram.programId, + systemProgram: SystemProgram.programId, + }) + .signers([originalAuthority]) + .rpc(); + + const postRandomHash = randombytes(32); + const postSeeds = [Buffer.from("post"), postRandomHash]; + const [postAccount, _bump] = await anchor.web3.PublicKey.findProgramAddress( + postSeeds, + program.programId + ); + + const metadataUri = + "https://gist.githubusercontent.com/abishekk92/10593977/raw/589238c3d48e654347d6cbc1e29c1e10dadc7cea/monoid.md"; + + const newPostIX = await program.methods + .createPost(metadataUri, postRandomHash) + .accounts({ + post: postAccount, + profile: profileAccount, + authority: originalAuthority.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([originalAuthority]) + .rpc(); + + lastPostHash = postRandomHash; + lastPostAccount = postAccount; + }); + + describe("Transfer Profile", async () => { + it("should execute transfer", async () => { + await program.methods + .transferProfile() + .accounts({ + profile: profileAccount, + invitation: originalAuthorityInvite, + newAuthorityInvitation: newAuthorityInvite, + authority: originalAuthority.publicKey, + newAuthority: newAuthority.publicKey, + systemProgram: SystemProgram.programId, + invitationProgram: invitationProgram.programId, + }) + .signers([originalAuthority]) + .rpc(); + + const profileData = await program.account.profile.fetch(profileAccount); + expect(profileData.authority.toString()).to.equal( + newAuthority.publicKey.toString() + ); + }); + + it("should not allow the old authority to edit", async () => { + const metadataUri = + "https://gist.githubusercontent.com/shekdev/10593977/raw/589238c3d48e654347d6cbc1e29c1e10dadc7cea/monoid.md"; + try { + await program.methods + .updatePost(metadataUri) + .accounts({ + post: lastPostAccount, + profile: profileAccount, + authority: originalAuthority.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([originalAuthority]) + .rpc(); + } catch (e) { + const { error } = AnchorError.parse(e.logs); + expect(error.errorMessage).to.equal( + "A has one constraint was violated" + ); + } + }); + + it("should allow the new authority to edit", async () => { + const metadataUri = + "https://gist.githubusercontent.com/shekdev/10593977/raw/589238c3d48e654347d6cbc1e29c1e10dadc7cea/monoid.md"; + await program.methods + .updatePost(metadataUri) + .accounts({ + post: lastPostAccount, + profile: profileAccount, + authority: newAuthority.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([newAuthority]) + .rpc(); + const postData = await program.account.post.fetch(lastPostAccount); + expect(postData.metadataUri).to.equal(metadataUri); + }); + + it("should not allow the old authority to create a new post", async () => { + const postRandomHash = randombytes(32); + const postSeeds = [Buffer.from("post"), postRandomHash]; + const [postAccount, _bump] = + await anchor.web3.PublicKey.findProgramAddress( + postSeeds, + program.programId + ); + const metadataUri = + "https://gist.githubusercontent.com/shekdev/10593977/raw/589238c3d48e654347d6cbc1e29c1e10dadc7cea/monoid.md"; + try { + await program.methods + .createPost(metadataUri, postRandomHash) + .accounts({ + post: postAccount, + profile: profileAccount, + authority: originalAuthority.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([originalAuthority]) + .rpc(); + } catch (e) { + const { error } = AnchorError.parse(e.logs); + expect(error.errorMessage).to.equal( + "A has one constraint was violated" + ); + } + }); + + it("should allow the new authority to create a new post", async () => { + const postRandomHash = randombytes(32); + const postSeeds = [Buffer.from("post"), postRandomHash]; + const [postAccount, _bump] = + await anchor.web3.PublicKey.findProgramAddress( + postSeeds, + program.programId + ); + const metadataUri = + "https://gist.githubusercontent.com/shekdev/10593977/raw/589238c3d48e654347d6cbc1e29c1e10dadc7cea/monoid.md"; + await program.methods + .createPost(metadataUri, postRandomHash) + .accounts({ + post: postAccount, + profile: profileAccount, + authority: newAuthority.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([newAuthority]) + .rpc(); + const postData = await program.account.post.fetch(postAccount); + expect(postData.metadataUri).to.equal(metadataUri); + }); + }); + + describe("Transfer Connections", async () => { + it("should transfer connections", async () => { + const connectionBoxHash = randombytes(32); + const connectionBoxSeeds = [ + Buffer.from("connection_box"), + connectionBoxHash, + ]; + const [connectionBoxAccount, _] = + await anchor.web3.PublicKey.findProgramAddress( + connectionBoxSeeds, + program.programId + ); + + const newConnectionBoxIX = await program.methods + .initializeConnectionBox(connectionBoxHash) + .accounts({ + connectionBox: connectionBoxAccount, + authority: originalAuthority.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([originalAuthority]) + .instruction(); + + await program.methods + .transferConnectionBox() + .accounts({ + connectionBox: connectionBoxAccount, + authority: originalAuthority.publicKey, + newAuthority: newAuthority.publicKey, + systemProgram: SystemProgram.programId, + }) + .preInstructions([newConnectionBoxIX]) + .signers([originalAuthority]) + .rpc(); + + const connectionBoxData = await program.account.connectionBox.fetch( + connectionBoxAccount + ); + expect(connectionBoxData.authority.toString()).to.equal( + newAuthority.publicKey.toString() + ); + }); + }); +}); diff --git a/tests/wordcel.spec.ts b/tests/wordcel.spec.ts index bef1ee8..81e234f 100644 --- a/tests/wordcel.spec.ts +++ b/tests/wordcel.spec.ts @@ -22,20 +22,22 @@ describe("wordcel", async () => { let onePostAccount: PublicKey; let inviteAccount: PublicKey; + before(async () => { + // Initialize Invitation Account + inviteAccount = await getInviteAccount(user); + await invitationProgram.methods + .initialize() + .accounts({ + inviteAccount: inviteAccount, + authority: user, + payer: user, + systemProgram: SystemProgram.programId, + }) + .rpc(); + }); + describe("Profile", async () => { it("should initialize", async () => { - // Initialize Invitation Account - inviteAccount = await getInviteAccount(user); - await invitationProgram.methods - .initialize() - .accounts({ - inviteAccount: inviteAccount, - authority: user, - payer: user, - systemProgram: SystemProgram.programId, - }) - .rpc(); - await program.methods .initialize(randomHash) .accounts({ @@ -61,29 +63,17 @@ describe("wordcel", async () => { ); const metadataUri = "https://gist.githubusercontent.com/abishekk92/10593977/raw/589238c3d48e654347d6cbc1e29c1e10dadc7cea/monoid.md"; - let listener = null; - let [event, slot] = await new Promise(async (resolve) => { - listener = program.addEventListener("NewPost", async (event, slot) => { - resolve([event, slot]); - }); - // Create Post - await program.methods - .createPost(metadataUri, randomHash) - .accounts({ - post: postAccount, - profile: profileAccount, - authority: user, - systemProgram: SystemProgram.programId, - }) - .rpc(); - }); - await program.removeEventListener(listener); - + await program.methods + .createPost(metadataUri, randomHash) + .accounts({ + post: postAccount, + profile: profileAccount, + authority: user, + systemProgram: SystemProgram.programId, + }) + .rpc(); const post = await program.account.post.fetch(postAccount); expect(post.metadataUri).to.equal(metadataUri); - expect(event.post.toString()).to.equal(postAccount.toString()); - expect(event.profile.toString()).to.equal(post.profile.toString()); - expect(slot).to.be.above(0); onePostAccount = postAccount; }); @@ -189,40 +179,20 @@ describe("wordcel", async () => { }); it("should create a connection", async () => { - let listener = null; - let [event, slot] = await new Promise(async (resolve) => { - listener = program.addEventListener( - "NewFollower", - async (event, slot) => { - resolve([event, slot]); - } - ); - // Initialize Connection - const tx = await program.methods - .initializeConnection() - .accounts({ - connection: connectionAccount, - profile: profileAccount, - authority: randomUser.publicKey, - systemProgram: SystemProgram.programId, - }) - .transaction(); - tx.feePayer = user; - tx.recentBlockhash = ( - await provider.connection.getRecentBlockhash() - ).blockhash; - tx.sign(randomUser); - await provider.sendAndConfirm(tx); - }); - await program.removeEventListener(listener); - + await program.methods + .initializeConnection() + .accounts({ + connection: connectionAccount, + profile: profileAccount, + authority: randomUser.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([randomUser]) + .rpc(); const connection = await program.account.connection.fetch( connectionAccount ); expect(connection.profile.toString()).to.equal(profileAccount.toString()); - expect(event.user.toString()).to.equal(connection.authority.toString()); - expect(event.followed.toString()).to.equal(connectionAccount.toString()); - expect(slot).to.be.above(0); }); it("should not let a user to follow themselves", async () => { @@ -253,24 +223,18 @@ describe("wordcel", async () => { }); it("should not create a connection again", async () => { - const tx = await program.methods - .initializeConnection() - .accounts({ - connection: connectionAccount, - profile: profileAccount, - authority: randomUser.publicKey, - systemProgram: SystemProgram.programId, - }) - .transaction(); - tx.feePayer = user; - tx.recentBlockhash = ( - await provider.connection.getRecentBlockhash() - ).blockhash; - tx.sign(randomUser); try { - await provider.sendAndConfirm(tx); + await program.methods + .initializeConnection() + .accounts({ + connection: connectionAccount, + profile: profileAccount, + authority: randomUser.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([randomUser]) + .rpc(); } catch (error) { - expect(error).to.be.an("error"); expect(error.toString()).to.contain("custom program error: 0x0"); } }); @@ -279,30 +243,25 @@ describe("wordcel", async () => { // Test must be run synchronously and in the specified order to avoid attepting to close an account that doesn't exist. it("should not allow unauthorized closing of connection", async () => { const closeUser = anchor.web3.Keypair.generate(); - const tx = await program.methods - .closeConnection() - .accounts({ - connection: connectionAccount, - profile: profileAccount, - authority: closeUser.publicKey, - systemProgram: SystemProgram.programId, - }) - .transaction(); - tx.feePayer = user; - tx.recentBlockhash = ( - await provider.connection.getRecentBlockhash() - ).blockhash; - tx.sign(closeUser); try { - await provider.sendAndConfirm(tx); + await program.methods + .closeConnection() + .accounts({ + connection: connectionAccount, + profile: profileAccount, + authority: closeUser.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([closeUser]) + .rpc(); } catch (error) { - expect(error).to.be.an("error"); - expect(error.toString()).to.contain("custom program error: 0x7d6"); + const anchorError = AnchorError.parse(error.logs); + expect(anchorError.error.errorCode.code).to.equal("ConstraintSeeds"); } }); it("should only allow the user to close the connection", async () => { - const tx = await program.methods + await program.methods .closeConnection() .accounts({ connection: connectionAccount, @@ -310,13 +269,8 @@ describe("wordcel", async () => { authority: randomUser.publicKey, systemProgram: SystemProgram.programId, }) - .transaction(); - tx.feePayer = user; - tx.recentBlockhash = ( - await provider.connection.getRecentBlockhash() - ).blockhash; - tx.sign(randomUser); - await provider.sendAndConfirm(tx); + .signers([randomUser]) + .rpc(); try { await program.account.connection.fetch(connectionAccount); } catch (error) { @@ -326,4 +280,333 @@ describe("wordcel", async () => { }); }); }); + + describe("Connections V2", async () => { + const profileRandomHash = randombytes(32); + const [testProfileAccount, _bump] = + await anchor.web3.PublicKey.findProgramAddress( + [Buffer.from("profile"), profileRandomHash], + program.programId + ); + let testUser = anchor.web3.Keypair.generate(); + const connectionBoxHash = randombytes(32); + const connectionBoxSeeds = [ + Buffer.from("connection_box"), + connectionBoxHash, + ]; + + const connectionSeeds = [ + Buffer.from("connection"), + testUser.publicKey.toBuffer(), + testProfileAccount.toBuffer(), + ]; + + const connectionV2Seeds = [ + Buffer.from("connection_v2"), + testProfileAccount.toBuffer(), + ]; + + const [connectionBoxAccount, _bump1] = + await anchor.web3.PublicKey.findProgramAddress( + connectionBoxSeeds, + program.programId + ); + + const [connectionV2Account, _bump2] = + await anchor.web3.PublicKey.findProgramAddress( + connectionV2Seeds, + program.programId + ); + + const [connectionAccount, _bump3] = + await anchor.web3.PublicKey.findProgramAddress( + connectionSeeds, + program.programId + ); + + before(async () => { + await airdrop(testUser.publicKey); + await program.methods + .initialize(profileRandomHash) + .accounts({ + profile: testProfileAccount, + user: user, + invitation: inviteAccount, + invitationProgram: invitationProgram.programId, + systemProgram: SystemProgram.programId, + }) + .rpc(); + }); + + it("should initialize connection box", async () => { + await program.methods + .initializeConnectionBox(connectionBoxHash) + .accounts({ + connectionBox: connectionBoxAccount, + authority: testUser.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([testUser]) + .rpc(); + const connectionBoxData = await program.account.connectionBox.fetch( + connectionBoxAccount + ); + expect(connectionBoxData.authority.toString()).to.equal( + testUser.publicKey.toString() + ); + }); + + it("should initialize connections_v2", async () => { + await program.methods + .initializeConnectionV2() + .accounts({ + connection: connectionV2Account, + connectionBox: connectionBoxAccount, + profile: testProfileAccount, + authority: testUser.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([testUser]) + .rpc(); + const connectionV2Data = await program.account.connectionV2.fetch( + connectionV2Account + ); + expect(connectionV2Data.connectionBox.toString()).to.equal( + connectionBoxAccount.toString() + ); + }); + + it("should close connections_v2", async () => { + await program.methods + .closeConnectionV2() + .accounts({ + connection: connectionV2Account, + connectionBox: connectionBoxAccount, + profile: testProfileAccount, + authority: testUser.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([testUser]) + .rpc(); + + try { + await program.account.connectionV2.fetch(connectionV2Account); + } catch (error) { + expect(error.toString()).to.contain("Error: Account does not exist"); + } + }); + + it("should allow the user to migrate connections from v1 to v2", async () => { + const connectionV1IX = await program.methods + .initializeConnection() + .accounts({ + connection: connectionAccount, + profile: testProfileAccount, + authority: testUser.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([testUser]) + .instruction(); + + await program.methods + .migrateToConnectionv2() + .accounts({ + connectionV1: connectionAccount, + connectionV2: connectionV2Account, + profile: testProfileAccount, + connectionBox: connectionBoxAccount, + authority: testUser.publicKey, + systemProgram: SystemProgram.programId, + }) + .preInstructions([connectionV1IX]) + .signers([testUser]) + .rpc(); + + try { + await program.account.connection.fetch(connectionAccount); + } catch (error) { + expect(error.toString()).to.contain("Error: Account does not exist"); + } + const connectionV2Data = await program.account.connectionV2.fetch( + connectionV2Account + ); + expect(connectionV2Data.connectionBox.toString()).to.equal( + connectionBoxAccount.toString() + ); + await program.methods + .closeConnectionV2() + .accounts({ + connection: connectionV2Account, + connectionBox: connectionBoxAccount, + profile: testProfileAccount, + authority: testUser.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([testUser]) + .rpc(); + }); + + it("should allow the admin to seemelessly migrate connections from v1 to v2", async () => { + const randomUser = anchor.web3.Keypair.generate(); + await airdrop(randomUser.publicKey); + + const connectionBoxHash = randombytes(32); + const connectionBoxSeeds = [ + Buffer.from("connection_box"), + connectionBoxHash, + ]; + + const connectionSeeds = [ + Buffer.from("connection"), + randomUser.publicKey.toBuffer(), + testProfileAccount.toBuffer(), + ]; + + const connectionV2Seeds = [ + Buffer.from("connection_v2"), + testProfileAccount.toBuffer(), + ]; + + const [connectionBoxAccount, _bump1] = + await anchor.web3.PublicKey.findProgramAddress( + connectionBoxSeeds, + program.programId + ); + + const [connectionV2Account, _bump2] = + await anchor.web3.PublicKey.findProgramAddress( + connectionV2Seeds, + program.programId + ); + + const [connectionAccount, _bump3] = + await anchor.web3.PublicKey.findProgramAddress( + connectionSeeds, + program.programId + ); + + await program.methods + .initializeConnection() + .accounts({ + connection: connectionAccount, + profile: testProfileAccount, + authority: randomUser.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([randomUser]) + .rpc(); + + await program.methods + .migrateToConnectionv2Admin(connectionBoxHash) + .accounts({ + connectionV1: connectionAccount, + connectionV2: connectionV2Account, + profile: testProfileAccount, + connectionBox: connectionBoxAccount, + authority: randomUser.publicKey, + payer: user, + systemProgram: SystemProgram.programId, + }) + .rpc(); + + try { + await program.account.connection.fetch(connectionAccount); + } catch (error) { + expect(error.toString()).to.contain("Error: Account does not exist"); + } + const connectionBoxData = await program.account.connectionBox.fetch( + connectionBoxAccount + ); + expect(connectionBoxData.authority.toString()).to.equal( + randomUser.publicKey.toString() + ); + + const connectionV2Data = await program.account.connectionV2.fetch( + connectionV2Account + ); + expect(connectionV2Data.connectionBox.toString()).to.equal( + connectionBoxAccount.toString() + ); + await program.methods + .closeConnectionV2() + .accounts({ + connection: connectionV2Account, + connectionBox: connectionBoxAccount, + profile: testProfileAccount, + authority: randomUser.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([randomUser]) + .rpc(); + }); + + it("should not allow anyone other than the admin to seemelessly migrate connections from v1 to v2", async () => { + const randomUser = anchor.web3.Keypair.generate(); + await airdrop(randomUser.publicKey); + + const connectionBoxHash = randombytes(32); + const connectionBoxSeeds = [ + Buffer.from("connection_box"), + connectionBoxHash, + ]; + + const connectionSeeds = [ + Buffer.from("connection"), + randomUser.publicKey.toBuffer(), + testProfileAccount.toBuffer(), + ]; + + const connectionV2Seeds = [ + Buffer.from("connection_v2"), + testProfileAccount.toBuffer(), + ]; + + const [connectionBoxAccount, _bump1] = + await anchor.web3.PublicKey.findProgramAddress( + connectionBoxSeeds, + program.programId + ); + + const [connectionV2Account, _bump2] = + await anchor.web3.PublicKey.findProgramAddress( + connectionV2Seeds, + program.programId + ); + const [connectionAccount, _bump3] = + await anchor.web3.PublicKey.findProgramAddress( + connectionSeeds, + program.programId + ); + + await program.methods + .initializeConnection() + .accounts({ + connection: connectionAccount, + profile: testProfileAccount, + authority: randomUser.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([randomUser]) + .rpc(); + + try { + await program.methods + .migrateToConnectionv2Admin(connectionBoxHash) + .accounts({ + connectionV1: connectionAccount, + connectionV2: connectionV2Account, + profile: testProfileAccount, + connectionBox: connectionBoxAccount, + authority: randomUser.publicKey, + payer: testUser.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([testUser]) + .rpc(); + } catch (e) { + const { error } = AnchorError.parse(e.logs); + expect(error.errorCode.code).to.equal("UnAuthorizedAccess"); + } + }); + }); });