diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 62b4844..6c9f1c5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,27 +1,85 @@ -name: test - +name: Run Tests on: push: - branches: [main] + branches: + - main pull_request: - branches: [main] env: - CARGO_TERM_COLOR: always + solana_version: v1.18.8 jobs: - build: + install: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + name: cache solana cli + id: cache-solana + with: + path: | + ~/.cache/solana/ + ~/.local/share/solana/ + key: solana-${{ runner.os }}-v0000-${{ env.solana_version }} + + - name: install essentials + run: | + sudo apt-get update + sudo apt-get install -y pkg-config build-essential libudev-dev + npm install --global yarn + + - name: install rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + + - name: Cache rust + uses: Swatinem/rust-cache@v2 + + - name: install solana + if: steps.cache-solana.outputs.cache-hit != 'true' + run: | + sh -c "$(curl -sSfL https://release.solana.com/${{ env.solana_version }}/install)" + export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH" + solana --version + + lint: + needs: install runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Install Solana CLI + - name: Run fmt + run: cargo fmt -- --check + - name: Run clippy + run: cargo clippy + + test: + needs: [install, lint] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + name: cache solana cli + id: cache-solana + with: + path: | + ~/.cache/solana/ + ~/.local/share/solana/ + key: solana-${{ runner.os }}-v0000-${{ env.solana_version }} + + - name: setup solana run: | - sh -c "$(curl -sSfL https://release.solana.com/v1.18.4/install)" - echo 'export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH"' >> $HOME/.bashrc - - name: Build + export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH" + solana --version + solana-keygen new --silent --no-bip39-passphrase + + - name: run build + run: | + cargo build + + - name: run tests run: | export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH" - cargo build-bpf - - name: Run tests - run: cargo test --verbose + cargo test-sbf diff --git a/README.md b/README.md index 7bc1600..e6190ad 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# TinyBlob +# Pith diff --git a/src/consts.rs b/src/consts.rs index d769a3e..6ec881a 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -1,2 +1,4 @@ -// The seed for market PDAs. +// The seed for the market PDA. pub const MARKET: &[u8] = b"market"; +// The seed for the bid PDA. +pub const BID: &[u8] = b"bid"; diff --git a/src/instruction.rs b/src/instruction.rs index 0f20f29..3ca7ca3 100644 --- a/src/instruction.rs +++ b/src/instruction.rs @@ -8,18 +8,28 @@ use shank::ShankInstruction; pub enum PithInstruction { #[account(0, name = "signer", desc = "Signer", signer)] #[account(1, name = "market", desc = "Pith market account", writable)] - #[account(2, name = "system_program", desc = "Solana System Program", writable)] - CreateMarket = 0, + #[account(2, name = "system_program", desc = "Solana System Program")] + Market = 0, - DeleteMarket = 1, - - UpdateMarket = 2, + #[account(0, name = "signer", desc = "Signer", signer)] + #[account(1, name = "market", desc = "Pith market account", writable)] + #[account(2, name = "bid", desc = "Bid account", writable)] + #[account(3, name = "system_program", desc = "Solana System Program")] + Bid = 1, } #[repr(C)] #[derive(Clone, Debug, BorshDeserialize)] -pub struct CreateMarketArgs { +pub struct MarketArgs { pub bump: u8, pub id: u64, pub title: String, } + +#[repr(C)] +#[derive(Clone, Debug, BorshDeserialize)] +pub struct BidArgs { + pub bump: u8, + pub id: u64, + pub amount: u64, +} diff --git a/src/lib.rs b/src/lib.rs index 9de7c75..43a9a48 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,9 +33,8 @@ pub fn process_instruction( .ok_or(ProgramError::InvalidInstructionData)?; match PithInstruction::try_from(*tag).or(Err(ProgramError::InvalidInstructionData))? { - PithInstruction::CreateMarket => process_init_market(program_id, accounts, data)?, - PithInstruction::DeleteMarket => process_delete_market(program_id, accounts, data)?, - PithInstruction::UpdateMarket => process_update_market(program_id, accounts, data)?, + PithInstruction::Market => process_market(program_id, accounts, data)?, + PithInstruction::Bid => process_bid(program_id, accounts, data)?, } Ok(()) diff --git a/src/loaders.rs b/src/loaders.rs index 3645d08..4f32dc9 100644 --- a/src/loaders.rs +++ b/src/loaders.rs @@ -2,7 +2,8 @@ use solana_program::{ account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, system_program, }; -// load_signer throws an error if the account is not the signer. +/// Errors if: +/// The account is not the signer. pub fn load_signer<'a, 'info>(info: &AccountInfo<'info>) -> Result<(), ProgramError> { if !info.is_signer { return Err(ProgramError::MissingRequiredSignature); @@ -32,9 +33,9 @@ pub fn load_uninitialized_account<'a, 'info>( Ok(()) } -// load_uninitialized_pda will throw an error if; -// The keys do not match -// The bump does not match +/// Errors if: +/// The keys do not match +/// The bump does not match pub fn load_uninitialized_pda<'a, 'info>( info: &'a AccountInfo<'info>, seeds: &[&[u8]], diff --git a/src/processor/bid.rs b/src/processor/bid.rs new file mode 100644 index 0000000..a8402d8 --- /dev/null +++ b/src/processor/bid.rs @@ -0,0 +1,73 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::{ + account_info::AccountInfo, borsh1::try_from_slice_unchecked, entrypoint::ProgramResult, msg, + program_error::ProgramError, pubkey::Pubkey, system_program, +}; + +use crate::{ + instruction::BidArgs, + loaders::{load_program, load_signer, load_uninitialized_pda}, + state::{Bid, Market}, + utils::create_pda, + BID, +}; + +/// process_bid handles the creation of a new bid on a market. +pub fn process_bid<'a, 'info>( + _program_id: &Pubkey, + accounts: &'a [AccountInfo<'info>], + data: &[u8], +) -> ProgramResult { + // Parse args + let args = BidArgs::try_from_slice(data)?; + + // Load account data + let [signer, market_info, bid_info, system_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + let mut market_data = try_from_slice_unchecked::(&market_info.data.borrow())?; + + load_signer(signer)?; + load_uninitialized_pda( + bid_info, + &[ + BID, + market_data.id.to_le_bytes().as_ref(), + signer.key.as_ref(), + args.id.to_le_bytes().as_ref(), + ], + args.bump, + &crate::id(), + )?; + load_program(system_program, system_program::id())?; + + // create bid Program Derived Address. + create_pda( + bid_info, + &crate::id(), + Bid::SIZE, + &[ + BID, + market_data.id.to_le_bytes().as_ref(), + signer.key.as_ref(), + args.id.to_le_bytes().as_ref(), + &[args.bump], + ], + system_program, + signer, + )?; + + let mut bid_data = try_from_slice_unchecked::(&bid_info.data.borrow()).unwrap(); + + bid_data.discriminator = Bid::DISCRIMINATOR.to_string(); + bid_data.market = *market_info.key; + bid_data.amount = args.amount; + bid_data.authority = *signer.key; + bid_data.serialize(&mut &mut bid_info.data.borrow_mut()[..])?; + + market_data.counter += 1; + market_data.serialize(&mut &mut market_info.data.borrow_mut()[..])?; + + Ok(()) +} diff --git a/src/processor/market.rs b/src/processor/market.rs index 4d17384..55efb46 100644 --- a/src/processor/market.rs +++ b/src/processor/market.rs @@ -3,17 +3,17 @@ use solana_program::{ program_error::ProgramError, pubkey::Pubkey, system_program, }; -use crate::{instruction::CreateMarketArgs, loaders::*, state::Market, utils::*, MARKET}; +use crate::{instruction::MarketArgs, loaders::*, state::Market, utils::*, MARKET}; use borsh::{BorshDeserialize, BorshSerialize}; -// process_init_market creates a new tradable market. -pub fn process_init_market<'a, 'info>( +// process_market creates a new tradable market. +pub fn process_market<'a, 'info>( _program_id: &Pubkey, accounts: &'a [AccountInfo<'info>], data: &[u8], ) -> ProgramResult { // Parse args - let args = CreateMarketArgs::try_from_slice(data)?; + let args = MarketArgs::try_from_slice(data)?; // Load account data let [signer, market_info, system_program] = accounts else { @@ -33,11 +33,7 @@ pub fn process_init_market<'a, 'info>( create_pda( market_info, &crate::id(), - // Calculate how much space we need. - // 1 byte => bump - // 8 bytes => id - // 4 bytes + title.len() => title - 1 + 8 + (4 + args.title.len()), + Market::get_account_size(&args.title, &Market::DISCRIMINATOR.to_string()), &[ MARKET, signer.key.as_ref(), @@ -49,27 +45,14 @@ pub fn process_init_market<'a, 'info>( )?; let mut market_data = try_from_slice_unchecked::(&market_info.data.borrow()).unwrap(); + market_data.discriminator = Market::DISCRIMINATOR.to_string(); market_data.bump = args.bump; + market_data.authority = *signer.key; market_data.id = args.id; market_data.title = args.title; - + market_data.key = *market_info.key; + market_data.counter = 0; market_data.serialize(&mut &mut market_info.data.borrow_mut()[..])?; Ok(()) } - -pub fn process_delete_market<'a, 'info>( - _program_id: &Pubkey, - _accounts: &'a [AccountInfo<'info>], - _data: &[u8], -) -> ProgramResult { - todo!() -} - -pub fn process_update_market<'a, 'info>( - _program_id: &Pubkey, - _accounts: &'a [AccountInfo<'info>], - _data: &[u8], -) -> ProgramResult { - todo!() -} diff --git a/src/processor/mod.rs b/src/processor/mod.rs index 92ffd00..2452ec4 100644 --- a/src/processor/mod.rs +++ b/src/processor/mod.rs @@ -1,3 +1,5 @@ +mod bid; mod market; +pub use bid::*; pub use market::*; diff --git a/src/state/bid.rs b/src/state/bid.rs new file mode 100644 index 0000000..4fa415c --- /dev/null +++ b/src/state/bid.rs @@ -0,0 +1,21 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use shank::ShankAccount; +use solana_program::pubkey::Pubkey; + +#[repr(C)] +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, ShankAccount)] +pub struct Bid { + /// Account discriminator. + pub discriminator: String, + /// The market key associated with this bid. + pub market: Pubkey, + /// The amount of the bid in lamports. + pub amount: u64, + /// The account that placed this bid. + pub authority: Pubkey, +} + +impl Bid { + pub const DISCRIMINATOR: &'static str = "bid"; + pub const SIZE: usize = (4 + Bid::DISCRIMINATOR.len()) + 32 + 8 + 32; +} diff --git a/src/state/market.rs b/src/state/market.rs index 6f89ec9..c114b13 100644 --- a/src/state/market.rs +++ b/src/state/market.rs @@ -1,13 +1,33 @@ use borsh::{BorshDeserialize, BorshSerialize}; use shank::ShankAccount; +use solana_program::pubkey::Pubkey; -// Market is an account that tracks the current state of the market. +/// Market is the parent account that stores a tradable asset and keeps track of +/// the bids placed on the specific market via a counter. +#[repr(C)] #[derive(Debug, Clone, BorshSerialize, BorshDeserialize, ShankAccount)] pub struct Market { - // The proof PDAs bump. + /// Account discriminator + pub discriminator: String, + /// The market account PDA. pub bump: u8, - // Transaction ID used to keep track of client state. + /// The accounts authority. + pub authority: Pubkey, + /// The unique market ID. pub id: u64, - // A none-unique string used to identify a market. + /// The title string for a specific market. pub title: String, + /// Counter keeps track of the number of bids placed on this market. + pub counter: u16, + /// The market account key. Useful since `getMultipleAccountsInfo` does not + /// return a `keyedAccountInfo`. + pub key: Pubkey, +} + +impl Market { + pub const DISCRIMINATOR: &'static str = "market"; + + pub fn get_account_size(title: &String, discriminator: &String) -> usize { + return (4 + discriminator.len()) + 1 + 32 + 8 + (4 + title.len()) + 2 + 32; + } } diff --git a/src/state/mod.rs b/src/state/mod.rs index 92ffd00..2452ec4 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -1,3 +1,5 @@ +mod bid; mod market; +pub use bid::*; pub use market::*;