diff --git a/package.json b/package.json index aa7791b..47a00ef 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "idl:upgrade:wordcel:mainnet": "anchor idl upgrade -f target/idl/wordcel.json --provider.cluster mainnet --provider.wallet ~/.config/solana/wordcel_mainnet.json EXzAYHZ8xS6QJ6xGRsdKZXixoQBLsuMbmwJozm85jHp", "idl:upgrade:invite:mainnet": "anchor idl upgrade -f target/idl/invite.json --provider.cluster mainnet --provider.wallet ~/.config/solana/wordcel_mainnet.json Fc4q6ttyDHr11HjMHRvanG9SskeR24Q62egdwsUUMHLf", "idl:upgrade:wordcel:devnet": "anchor idl upgrade -f target/idl/wordcel.json --provider.cluster devnet --provider.wallet ~/.config/solana/wordcel_admin.json D9JJgeRf2rKq5LNMHLBMb92g4ZpeMgCyvZkd7QKwSCzg", - "idl:upgrade:invite:devnet": "anchor idl upgrade -f target/idl/invite.json --provider.cluster devnet --provider.wallet ~/.config/solana/wordcel_admin.json 6G5x4Es2YZYB5e4QkFJN88TrfLABkYEQpkUH5Gob9Cut" + "idl:upgrade:invite:devnet": "anchor idl upgrade -f target/idl/invite.json --provider.cluster devnet --provider.wallet ~/.config/solana/wordcel_admin.json 6G5x4Es2YZYB5e4QkFJN88TrfLABkYEQpkUH5Gob9Cut", + "anchor:publish:invite:mainnet": "anchor publish invite --provider.cluster mainnet --provider.wallet ~/.config/solana/wordcel_mainnet.json -- --features mainnet" }, "dependencies": { "@bundlr-network/client": "^0.5.12", diff --git a/programs/invite/src/lib.rs b/programs/invite/src/lib.rs index dd14f62..f2a1788 100644 --- a/programs/invite/src/lib.rs +++ b/programs/invite/src/lib.rs @@ -18,20 +18,20 @@ pub mod invite { pub fn initialize(ctx: Context) -> Result<()> { let invite_account = &mut ctx.accounts.invite_account; - invite_account.bump = *ctx.bumps.get("invite_account").unwrap(); + invite_account.bump = ctx.bumps["invite_account"]; invite_account.authority = *ctx.accounts.authority.to_account_info().key; invite_account.invited_by = *ctx.accounts.payer.to_account_info().key; - invite_account.invites_left = Invite::MAX_TO_GIVE; + invite_account.invites_left = Invite::NUM_INITIAL_INVITES; invite_account.invites_sent = 0; Ok(()) } pub fn send_invite(ctx: Context) -> Result<()> { let to_invite_account = &mut ctx.accounts.to_invite_account; - to_invite_account.bump = *ctx.bumps.get("to_invite_account").unwrap(); + to_invite_account.bump = ctx.bumps["to_invite_account"]; to_invite_account.authority = *ctx.accounts.to.to_account_info().key; to_invite_account.invited_by = *ctx.accounts.authority.to_account_info().key; - to_invite_account.invites_left = Invite::MAX_TO_GIVE; + to_invite_account.invites_left = Invite::NUM_INITIAL_INVITES; to_invite_account.invites_sent = 0; let invite_account = &mut ctx.accounts.invite_account; @@ -39,6 +39,12 @@ pub mod invite { invite_account.invites_left = invite_account.invites_left.checked_sub(1).unwrap(); Ok(()) } + + pub fn claim_invite(ctx: Context) -> Result<()> { + let invite_account = &mut ctx.accounts.invite_account; + invite_account.invites_left = invite_account.invites_left.checked_add(4).unwrap(); + Ok(()) + } } #[derive(Accounts)] @@ -93,6 +99,24 @@ pub struct SendInvite<'info> { pub system_program: Program<'info, System>, } +#[derive(Accounts)] +pub struct ClaimInvites<'info> { + #[account( + mut, + has_one = authority, + seeds = [ + Invite::PREFIX.as_bytes().as_ref(), + authority.key().as_ref() + ], + bump = invite_account.bump, + constraint = invite_account.invites_left <= 1 @InviteError::IneligibleClaim, + )] + pub invite_account: Account<'info, Invite>, + #[account(mut)] + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, +} + #[account] #[derive(Default)] pub struct Invite { @@ -105,7 +129,7 @@ pub struct Invite { impl Invite { pub const PREFIX: &'static str = "invite"; - pub const MAX_TO_GIVE: u8 = 2; + pub const NUM_INITIAL_INVITES: u8 = 2; pub const LEN: usize = 8 + size_of::(); fn is_whitelisted(key: Pubkey) -> bool { @@ -131,5 +155,7 @@ impl Invite { #[error_code] pub enum InviteError { NoInvitesLeft, + #[msg("Invites can be only claimed after existing invites are used")] + IneligibleClaim, UnAuthorizedInitialization, } diff --git a/tests/invite.spec.ts b/tests/invite.spec.ts index 9feaacd..6385ea4 100644 --- a/tests/invite.spec.ts +++ b/tests/invite.spec.ts @@ -1,137 +1,195 @@ -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 allow a user to claim more 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); + + await program.methods + .claimInvite() + .accounts({ + inviteAccount: inviteAccount, + authority: newUser.publicKey, + systemProgram: SystemProgram.programId, + }) + .signers([newUser]) + .rpc(); + const invite_data = await program.account.invite.fetch(inviteAccount); + expect(invite_data.invitesLeft).to.equal(4); + }); + + 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" + ); + } + }); });