diff --git a/.gitignore b/.gitignore index 870608d..e1625ac 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ target node_modules test_ledger/ .goki/ +scratch/ diff --git a/Anchor.toml b/Anchor.toml index 3d85c18..d8020b4 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -1,14 +1,17 @@ [programs.localnet] wordcel = "v4enuof3drNvU2Y3b5m7K62hMq3QUP6qQSV2jjxAhkp" invite = "6G5x4Es2YZYB5e4QkFJN88TrfLABkYEQpkUH5Gob9Cut" +slugger = "SAbD2TPKyTd54oahjz6UEBzweXvojsRWbGB2t21gDnB" [programs.devnet] wordcel = "D9JJgeRf2rKq5LNMHLBMb92g4ZpeMgCyvZkd7QKwSCzg" invite = "6G5x4Es2YZYB5e4QkFJN88TrfLABkYEQpkUH5Gob9Cut" +slugger = "SAbD2TPKyTd54oahjz6UEBzweXvojsRWbGB2t21gDnB" [programs.mainnet] wordcel = "EXzAYHZ8xS6QJ6xGRsdKZXixoQBLsuMbmwJozm85jHp" invite = "Fc4q6ttyDHr11HjMHRvanG9SskeR24Q62egdwsUUMHLf" +slugger = "SAbD2TPKyTd54oahjz6UEBzweXvojsRWbGB2t21gDnB" [registry] url = "https://anchor.projectserum.com" diff --git a/Cargo.lock b/Cargo.lock index aab438d..bc0c486 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -988,6 +988,14 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "slugger" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "wordcel", +] + [[package]] name = "smallvec" version = "1.8.0" diff --git a/programs/slugger/Cargo.toml b/programs/slugger/Cargo.toml new file mode 100644 index 0000000..5e79e30 --- /dev/null +++ b/programs/slugger/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "slugger" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "slugger" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = ["wordcel"] +devnet = ["wordcel", "wordcel/devnet"] +mainnet = ["wordcel", "wordcel/mainnet"] + +[profile.release] +overflow-checks = true + +[dependencies] +anchor-lang = "0.24.2" +wordcel = { path = "../wordcel", features = ["cpi"], optional = true } diff --git a/programs/slugger/Xargo.toml b/programs/slugger/Xargo.toml new file mode 100644 index 0000000..475fb71 --- /dev/null +++ b/programs/slugger/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/programs/slugger/src/lib.rs b/programs/slugger/src/lib.rs new file mode 100644 index 0000000..955ad17 --- /dev/null +++ b/programs/slugger/src/lib.rs @@ -0,0 +1,87 @@ +use anchor_lang::prelude::*; +use std::mem::size_of; + +use wordcel::program::Wordcel as WordcelProgram; +use wordcel::state::{Post, Profile}; + +declare_id!("SAbD2TPKyTd54oahjz6UEBzweXvojsRWbGB2t21gDnB"); + +#[program] +pub mod slugger { + use super::*; + + pub fn initialize(ctx: Context, slug_hash: [u8; 32]) -> Result<()> { + let slug = &mut ctx.accounts.slug; + slug.slug_hash = slug_hash; + slug.bump = *ctx.bumps.get("slug").unwrap(); + slug.authority = *ctx.accounts.authority.to_account_info().key; + slug.post = *ctx.accounts.post.to_account_info().key; + slug.profile = *ctx.accounts.profile.to_account_info().key; + Ok(()) + } +} + +#[derive(Accounts)] +#[instruction( + slug_hash: [u8;32], +)] + +// Question: Is it safe to read bump from the account, instead of recalculating it? +// What possible attack could it open up? +// Will it allow one to post on another profile? +// +// TODO: Verify with test cases +pub struct Initialize<'info> { + #[account( + init, + seeds = [ + b"slug".as_ref(), + profile.key().as_ref(), + &slug_hash + ], + bump, + payer = authority, + space = Slug::LEN + )] + pub slug: Account<'info, Slug>, + #[account( + owner = wordcel_program.key(), + seeds = [ + b"post".as_ref(), + post.random_hash.as_ref() + ], + seeds::program = wordcel_program.key(), + bump = post.bump, + has_one = profile + )] + pub post: Account<'info, Post>, + #[account( + owner = wordcel_program.key(), + seeds = [ + b"profile".as_ref(), + profile.random_hash.as_ref() + ], + seeds::program = wordcel_program.key(), + bump = profile.bump, + has_one = authority + )] + pub profile: Account<'info, Profile>, + #[account(mut)] + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, + pub wordcel_program: Program<'info, WordcelProgram>, +} + +#[account] +#[derive(Default)] +pub struct Slug { + slug_hash: [u8; 32], + post: Pubkey, + profile: Pubkey, + bump: u8, + authority: Pubkey, +} + +impl Slug { + pub const LEN: usize = 8 + size_of::(); +} diff --git a/tests/slugger.spec.ts b/tests/slugger.spec.ts new file mode 100644 index 0000000..2aedf47 --- /dev/null +++ b/tests/slugger.spec.ts @@ -0,0 +1,88 @@ +import * as anchor from '@project-serum/anchor'; +import {Program, AnchorError} from '@project-serum/anchor'; +import {Wordcel} from '../target/types/wordcel'; +import {Slugger} from '../target/types/slugger'; +import {expect} from 'chai'; +import {PublicKey} from '@solana/web3.js'; +import {getInviteSingleton, invitationProgram} from "./utils/invite"; +import {airdrop} from './utils'; +import randombytes from 'randombytes'; +import {createHash} from 'crypto'; + +const {SystemProgram} = anchor.web3; +const provider = anchor.getProvider(); + +const wordcelProgram = anchor.workspace.Wordcel as Program; +const sluggerProgram = anchor.workspace.Slugger as Program; + + +const user = provider.wallet.publicKey; + + +function getSlugHash(slug) { + return createHash('sha256').update(slug, 'utf8').digest(); +} +async function getSlugAccount(slugHash, profileAccount: PublicKey) { + const seeds = [Buffer.from("slug"), profileAccount.toBuffer(), slugHash]; + const [account, _] = await anchor.web3.PublicKey.findProgramAddress(seeds, sluggerProgram.programId); + return account +} + +describe('Slugger', async () => { + + let inviteAccount: PublicKey; + let profileAccount: PublicKey; + let postAccount: PublicKey; + + // Prepare test user. + before(async () => { + await airdrop(user); + inviteAccount = await getInviteSingleton(user); + + // Set up a profile + const profileHash = randombytes(32); + const profileSeed = [Buffer.from("profile"), profileHash]; + const [_profileAccount, _profileBump] = await anchor.web3.PublicKey.findProgramAddress(profileSeed, wordcelProgram.programId); + profileAccount = _profileAccount; + await wordcelProgram.methods.initialize(profileHash) + .accounts({ + profile: profileAccount, + user: user, + invitation: inviteAccount, + invitationProgram: invitationProgram.programId, + systemProgram: SystemProgram.programId + }).rpc(); + + // Set up a post + const postHash = randombytes(32); + const postSeeds = [Buffer.from("post"), postHash]; + const [_postAccount, _postBump] = await anchor.web3.PublicKey.findProgramAddress(postSeeds, wordcelProgram.programId); + postAccount = _postAccount; + const metadataUri = "https://gist.githubusercontent.com/abishekk92/10593977/raw/589238c3d48e654347d6cbc1e29c1e10dadc7cea/monoid.md"; + await wordcelProgram.methods.createPost(metadataUri, postHash).accounts({ + post: postAccount, + profile: profileAccount, + authority: user, + systemProgram: SystemProgram.programId, + }).rpc(); + }); + + + it("should initialize", async () => { + let slugHash = getSlugHash("gm-wagmi"); + let slugAccount = await getSlugAccount(slugHash, profileAccount); + await sluggerProgram.methods.initialize(slugHash) + .accounts({ + slug: slugAccount, + profile: profileAccount, + post: postAccount, + authority: user, + wordcelProgram: wordcelProgram.programId, + systemProgram: SystemProgram.programId + }) + .rpc(); + const data = await sluggerProgram.account.slug.fetch(slugAccount); + expect(data.authority.toString()).to.equal(user.toString()); + expect(data.post.toString()).to.equal(postAccount.toString()); + }); +}); diff --git a/tests/utils/invite.ts b/tests/utils/invite.ts index 1a12f83..b7e8468 100644 --- a/tests/utils/invite.ts +++ b/tests/utils/invite.ts @@ -7,6 +7,8 @@ const {SystemProgram} = anchor.web3; const provider = anchor.getProvider(); const invitationPrefix = Buffer.from("invite"); +let inviteMap = new Map(); + export const invitationProgram = anchor.workspace.Invite as Program; export async function getInviteAccount(key: PublicKey) { @@ -33,3 +35,20 @@ export async function sendInvite(from_user: Keypair, to: PublicKey, feePayer: Pu await provider.sendAndConfirm(tx); return [inviteAccount, toInviteAccount]; } + +export async function getInviteSingleton(user: PublicKey) { + const key = user.toString() + if(inviteMap.has(key)) { + return inviteMap.get(key) + } + const inviteAccount = await getInviteAccount(user); + await invitationProgram.methods.initialize() + .accounts({ + inviteAccount: inviteAccount, + authority: user, + payer: user, + systemProgram: SystemProgram.programId + }).rpc(); + inviteMap.set(key, inviteAccount); + return inviteAccount; +} diff --git a/yarn.lock b/yarn.lock index 2a52bb3..6837e1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1040,6 +1040,11 @@ crypto-hash@^1.3.0: resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz" integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg== +crypto-js@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf" + integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw== + debug@4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz"