diff --git a/programs/clearing_house/src/context.rs b/programs/clearing_house/src/context.rs index 23b86782..54cd1699 100644 --- a/programs/clearing_house/src/context.rs +++ b/programs/clearing_house/src/context.rs @@ -13,6 +13,7 @@ use crate::state::order_state::OrderState; use crate::state::state::State; use crate::state::user::{User, UserPositions}; use crate::state::user_orders::{OrderTriggerCondition, OrderType, UserOrders}; +use crate::state::user_registry::UserRegistry; #[derive(Accounts)] #[instruction( @@ -148,6 +149,68 @@ pub struct InitializeUserWithExplicitPayer<'info> { pub system_program: Program<'info, System>, } +#[derive(Accounts)] +#[instruction(user_registry_nonce: u8)] +pub struct InitializeUserRegistry<'info> { + #[account( + init, + seeds = [b"user_registry", authority.key.as_ref()], + bump = user_registry_nonce, + payer = authority + )] + pub user_registry: Box>, + #[account(mut)] + pub authority: Signer<'info>, + #[account( + has_one = authority, + )] + pub user: Box>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +#[instruction(user_seed: u8, user_nonce: u8)] +pub struct AddUser<'info> { + #[account( + mut, + has_one = authority + )] + pub user_registry: Box>, + #[account( + init, + seeds = [ + b"user", + user_seed.to_le_bytes().as_ref(), + authority.key.as_ref() + ], + bump = user_nonce, + payer = authority + )] + pub user: Box>, + pub state: Box>, + #[account( + init, + payer = authority, + )] + pub user_positions: AccountLoader<'info, UserPositions>, + #[account(mut)] + pub authority: Signer<'info>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct UpdateUserName<'info> { + #[account( + mut, + has_one = authority + )] + pub user_registry: Box>, + #[account(mut)] + pub authority: Signer<'info>, +} + #[derive(Accounts)] #[instruction(user_orders_nonce: u8)] pub struct InitializeUserOrders<'info> { @@ -236,7 +299,6 @@ pub struct InitializeMarket<'info> { #[derive(Accounts)] pub struct DepositCollateral<'info> { - #[account(mut)] pub state: Box>, #[account( mut, @@ -276,7 +338,6 @@ pub struct DepositCollateral<'info> { #[derive(Accounts)] pub struct WithdrawCollateral<'info> { - #[account(mut)] pub state: Box>, #[account( mut, @@ -327,6 +388,48 @@ pub struct WithdrawCollateral<'info> { pub deposit_history: AccountLoader<'info, DepositHistory>, } +#[derive(Accounts)] +pub struct TransferCollateral<'info> { + pub state: Box>, + pub authority: Signer<'info>, + #[account( + mut, + has_one = authority, + constraint = &from_user.positions.eq(&from_user_positions.key()) + )] + pub from_user: Box>, + #[account( + mut, + constraint = &from_user_positions.load()?.user.eq(&from_user.key()) + )] + pub from_user_positions: AccountLoader<'info, UserPositions>, + #[account( + mut, + has_one = authority, + constraint = &to_user.positions.eq(&to_user_positions.key()) + )] + pub to_user: Box>, + #[account( + mut, + constraint = &to_user_positions.load()?.user.eq(&to_user.key()) + )] + pub to_user_positions: AccountLoader<'info, UserPositions>, + #[account( + constraint = &state.markets.eq(&markets.key()) + )] + pub markets: AccountLoader<'info, Markets>, + #[account( + mut, + constraint = &state.funding_payment_history.eq(&funding_payment_history.key()) + )] + pub funding_payment_history: AccountLoader<'info, FundingPaymentHistory>, + #[account( + mut, + constraint = &state.deposit_history.eq(&deposit_history.key()) + )] + pub deposit_history: AccountLoader<'info, DepositHistory>, +} + #[derive(Accounts)] pub struct WithdrawFees<'info> { #[account( diff --git a/programs/clearing_house/src/controller/deposits.rs b/programs/clearing_house/src/controller/deposits.rs new file mode 100644 index 00000000..957494cb --- /dev/null +++ b/programs/clearing_house/src/controller/deposits.rs @@ -0,0 +1,264 @@ +use crate::controller::funding::settle_funding_payment; +use crate::controller::token::{receive, send}; +use crate::error::*; +use crate::math::casting::{cast, cast_to_u128}; +use crate::math::margin::meets_initial_margin_requirement; +use crate::math::withdrawal::calculate_withdrawal_amounts; +use crate::math_error; +use crate::state::history::deposit::{DepositDirection, DepositHistory, DepositRecord}; +use crate::state::history::funding_payment::FundingPaymentHistory; +use crate::state::market::Markets; +use crate::state::state::State; +use crate::state::user::{User, UserPositions}; +use anchor_lang::prelude::*; +use anchor_spl::token::{Token, TokenAccount}; +use solana_program::msg; + +pub fn deposit_collateral<'info>( + amount: u64, + state: &State, + authority: &Signer<'info>, + user: &mut Box>, + user_positions: &mut AccountLoader, + markets: &AccountLoader, + token_program: &Program<'info, Token>, + user_collateral_account: &Account<'info, TokenAccount>, + collateral_vault: &Account<'info, TokenAccount>, + funding_payment_history: &AccountLoader, + deposit_history: &AccountLoader, + clock: &Clock, +) -> ProgramResult { + let now = clock.unix_timestamp; + + if amount == 0 { + return Err(ErrorCode::InsufficientDeposit.into()); + } + + let collateral_before = user.collateral; + let cumulative_deposits_before = user.cumulative_deposits; + + user.collateral = user + .collateral + .checked_add(cast(amount)?) + .ok_or_else(math_error!())?; + user.cumulative_deposits = user + .cumulative_deposits + .checked_add(cast(amount)?) + .ok_or_else(math_error!())?; + + let markets = &markets.load()?; + let user_positions = &mut user_positions.load_mut()?; + let funding_payment_history = &mut funding_payment_history.load_mut()?; + settle_funding_payment(user, user_positions, markets, funding_payment_history, now)?; + + receive( + token_program, + user_collateral_account, + collateral_vault, + authority, + amount, + )?; + + let deposit_history = &mut deposit_history.load_mut()?; + let record_id = deposit_history.next_record_id(); + deposit_history.append(DepositRecord { + ts: now, + record_id, + user_authority: user.authority, + user: user.to_account_info().key(), + direction: DepositDirection::Deposit, + collateral_before, + cumulative_deposits_before, + amount, + }); + + if state.max_deposit > 0 && user.cumulative_deposits > cast(state.max_deposit)? { + return Err(ErrorCode::UserMaxDeposit.into()); + } + + Ok(()) +} + +pub fn withdraw_collateral<'info>( + amount: u64, + state: &State, + user: &mut Box>, + user_positions: &mut AccountLoader, + markets: &AccountLoader, + token_program: &Program<'info, Token>, + user_collateral_account: &Account<'info, TokenAccount>, + collateral_vault: &Account<'info, TokenAccount>, + collateral_vault_authority: &AccountInfo<'info>, + insurance_vault: &Account<'info, TokenAccount>, + insurance_vault_authority: &AccountInfo<'info>, + funding_payment_history: &AccountLoader, + deposit_history: &AccountLoader, + clock: &Clock, +) -> ProgramResult { + let now = clock.unix_timestamp; + + let collateral_before = user.collateral; + let cumulative_deposits_before = user.cumulative_deposits; + + let markets = &markets.load()?; + let user_positions = &mut user_positions.load_mut()?; + let funding_payment_history = &mut funding_payment_history.load_mut()?; + settle_funding_payment(user, user_positions, markets, funding_payment_history, now)?; + + if cast_to_u128(amount)? > user.collateral { + return Err(ErrorCode::InsufficientCollateral.into()); + } + + let (collateral_account_withdrawal, insurance_account_withdrawal) = + calculate_withdrawal_amounts(amount, collateral_vault, insurance_vault)?; + + // amount_withdrawn can be less than amount if there is an insufficient balance in collateral and insurance vault + let amount_withdraw = collateral_account_withdrawal + .checked_add(insurance_account_withdrawal) + .ok_or_else(math_error!())?; + + user.cumulative_deposits = user + .cumulative_deposits + .checked_sub(cast(amount_withdraw)?) + .ok_or_else(math_error!())?; + + user.collateral = user + .collateral + .checked_sub(cast(collateral_account_withdrawal)?) + .ok_or_else(math_error!())? + .checked_sub(cast(insurance_account_withdrawal)?) + .ok_or_else(math_error!())?; + + if !meets_initial_margin_requirement(user, user_positions, markets)? { + return Err(ErrorCode::InsufficientCollateral.into()); + } + + send( + token_program, + collateral_vault, + user_collateral_account, + collateral_vault_authority, + state.collateral_vault_nonce, + collateral_account_withdrawal, + )?; + + if insurance_account_withdrawal > 0 { + send( + token_program, + insurance_vault, + user_collateral_account, + insurance_vault_authority, + state.insurance_vault_nonce, + insurance_account_withdrawal, + )?; + } + + let deposit_history = &mut deposit_history.load_mut()?; + let record_id = deposit_history.next_record_id(); + deposit_history.append(DepositRecord { + ts: now, + record_id, + user_authority: user.authority, + user: user.to_account_info().key(), + direction: DepositDirection::Withdraw, + collateral_before, + cumulative_deposits_before, + amount: amount_withdraw, + }); + + Ok(()) +} + +pub fn transfer_collateral( + amount: u64, + from_user: &mut Box>, + from_user_positions: &mut AccountLoader, + to_user: &mut Box>, + to_user_positions: &mut AccountLoader, + markets: &AccountLoader, + funding_payment_history: &AccountLoader, + deposit_history: &AccountLoader, + clock: &Clock, +) -> ProgramResult { + let now = clock.unix_timestamp; + + let from_user_collateral_before = from_user.collateral; + let from_user_cumulative_deposits_before = from_user.cumulative_deposits; + + let markets = &markets.load()?; + let from_user_positions = &mut from_user_positions.load_mut()?; + let funding_payment_history = &mut funding_payment_history.load_mut()?; + settle_funding_payment( + from_user, + from_user_positions, + markets, + funding_payment_history, + now, + )?; + + if cast_to_u128(amount)? > from_user.collateral { + return Err(ErrorCode::InsufficientCollateral.into()); + } + + from_user.cumulative_deposits = from_user + .cumulative_deposits + .checked_sub(cast(amount)?) + .ok_or_else(math_error!())?; + + from_user.collateral = from_user + .collateral + .checked_sub(cast(amount)?) + .ok_or_else(math_error!())?; + + if !meets_initial_margin_requirement(from_user, from_user_positions, markets)? { + return Err(ErrorCode::InsufficientCollateral.into()); + } + + let deposit_history = &mut deposit_history.load_mut()?; + let record_id = deposit_history.next_record_id(); + deposit_history.append(DepositRecord { + ts: now, + record_id, + user_authority: from_user.authority, + user: from_user.to_account_info().key(), + direction: DepositDirection::TransferOut, + collateral_before: from_user_collateral_before, + cumulative_deposits_before: from_user_cumulative_deposits_before, + amount, + }); + + let to_user_collateral_before = to_user.collateral; + let to_user_cumulative_deposits_before = to_user.cumulative_deposits; + + to_user.collateral = to_user + .collateral + .checked_add(cast(amount)?) + .ok_or_else(math_error!())?; + to_user.cumulative_deposits = to_user + .cumulative_deposits + .checked_add(cast(amount)?) + .ok_or_else(math_error!())?; + + let to_user_positions = &mut to_user_positions.load_mut()?; + settle_funding_payment( + to_user, + to_user_positions, + markets, + funding_payment_history, + now, + )?; + + let record_id = deposit_history.next_record_id(); + deposit_history.append(DepositRecord { + ts: now, + record_id, + user_authority: to_user.authority, + user: to_user.to_account_info().key(), + direction: DepositDirection::TransferIn, + collateral_before: to_user_collateral_before, + cumulative_deposits_before: to_user_cumulative_deposits_before, + amount, + }); + + Ok(()) +} diff --git a/programs/clearing_house/src/controller/mod.rs b/programs/clearing_house/src/controller/mod.rs index 69ea7b57..919e0d94 100644 --- a/programs/clearing_house/src/controller/mod.rs +++ b/programs/clearing_house/src/controller/mod.rs @@ -1,4 +1,5 @@ pub mod amm; +pub mod deposits; pub mod funding; pub mod orders; pub mod position; diff --git a/programs/clearing_house/src/error.rs b/programs/clearing_house/src/error.rs index cb720fab..b9a2b1fc 100644 --- a/programs/clearing_house/src/error.rs +++ b/programs/clearing_house/src/error.rs @@ -126,6 +126,10 @@ pub enum ErrorCode { InvalidOracleOffset, #[msg("CantExpireOrders")] CantExpireOrders, + #[msg("InvalidUserName")] + InvalidUserName, + #[msg("UserSeedAlreadyUsed")] + InvalidUserSeed, } #[macro_export] diff --git a/programs/clearing_house/src/lib.rs b/programs/clearing_house/src/lib.rs index 2be6d913..62eadd1d 100644 --- a/programs/clearing_house/src/lib.rs +++ b/programs/clearing_house/src/lib.rs @@ -52,6 +52,7 @@ pub mod clearing_house { use crate::math::slippage::{calculate_slippage, calculate_slippage_pct}; use crate::state::market::OraclePriceData; use crate::state::order_state::{OrderFillerRewardStructure, OrderState}; + use crate::state::user_registry::{is_valid_name, UNINITIALIZED_NAME}; use std::ops::Div; pub fn initialize( @@ -351,155 +352,59 @@ pub mod clearing_house { } pub fn deposit_collateral(ctx: Context, amount: u64) -> ProgramResult { - let user = &mut ctx.accounts.user; - let clock = Clock::get()?; - let now = clock.unix_timestamp; - - if amount == 0 { - return Err(ErrorCode::InsufficientDeposit.into()); - } - - let collateral_before = user.collateral; - let cumulative_deposits_before = user.cumulative_deposits; - - user.collateral = user - .collateral - .checked_add(cast(amount)?) - .ok_or_else(math_error!())?; - user.cumulative_deposits = user - .cumulative_deposits - .checked_add(cast(amount)?) - .ok_or_else(math_error!())?; - - let markets = &ctx.accounts.markets.load()?; - let user_positions = &mut ctx.accounts.user_positions.load_mut()?; - let funding_payment_history = &mut ctx.accounts.funding_payment_history.load_mut()?; - controller::funding::settle_funding_payment( - user, - user_positions, - markets, - funding_payment_history, - now, - )?; - - controller::token::receive( + controller::deposits::deposit_collateral( + amount, + &ctx.accounts.state, + &ctx.accounts.authority, + &mut ctx.accounts.user, + &mut ctx.accounts.user_positions, + &ctx.accounts.markets, &ctx.accounts.token_program, &ctx.accounts.user_collateral_account, &ctx.accounts.collateral_vault, - &ctx.accounts.authority, - amount, - )?; - - let deposit_history = &mut ctx.accounts.deposit_history.load_mut()?; - let record_id = deposit_history.next_record_id(); - deposit_history.append(DepositRecord { - ts: now, - record_id, - user_authority: user.authority, - user: user.to_account_info().key(), - direction: DepositDirection::DEPOSIT, - collateral_before, - cumulative_deposits_before, - amount, - }); - - if ctx.accounts.state.max_deposit > 0 - && user.cumulative_deposits > cast(ctx.accounts.state.max_deposit)? - { - return Err(ErrorCode::UserMaxDeposit.into()); - } - - Ok(()) + &ctx.accounts.funding_payment_history, + &ctx.accounts.deposit_history, + &Clock::get()?, + ) } #[access_control( exchange_not_paused(&ctx.accounts.state) )] pub fn withdraw_collateral(ctx: Context, amount: u64) -> ProgramResult { - let user = &mut ctx.accounts.user; - let clock = Clock::get()?; - let now = clock.unix_timestamp; - - let collateral_before = user.collateral; - let cumulative_deposits_before = user.cumulative_deposits; - - let markets = &ctx.accounts.markets.load()?; - let user_positions = &mut ctx.accounts.user_positions.load_mut()?; - let funding_payment_history = &mut ctx.accounts.funding_payment_history.load_mut()?; - controller::funding::settle_funding_payment( - user, - user_positions, - markets, - funding_payment_history, - now, - )?; - - if cast_to_u128(amount)? > user.collateral { - return Err(ErrorCode::InsufficientCollateral.into()); - } - - let (collateral_account_withdrawal, insurance_account_withdrawal) = - calculate_withdrawal_amounts( - amount, - &ctx.accounts.collateral_vault, - &ctx.accounts.insurance_vault, - )?; - - // amount_withdrawn can be less than amount if there is an insufficient balance in collateral and insurance vault - let amount_withdraw = collateral_account_withdrawal - .checked_add(insurance_account_withdrawal) - .ok_or_else(math_error!())?; - - user.cumulative_deposits = user - .cumulative_deposits - .checked_sub(cast(amount_withdraw)?) - .ok_or_else(math_error!())?; - - user.collateral = user - .collateral - .checked_sub(cast(collateral_account_withdrawal)?) - .ok_or_else(math_error!())? - .checked_sub(cast(insurance_account_withdrawal)?) - .ok_or_else(math_error!())?; - - if !meets_initial_margin_requirement(user, user_positions, markets)? { - return Err(ErrorCode::InsufficientCollateral.into()); - } - - controller::token::send( + controller::deposits::withdraw_collateral( + amount, + &ctx.accounts.state, + &mut ctx.accounts.user, + &mut ctx.accounts.user_positions, + &ctx.accounts.markets, &ctx.accounts.token_program, - &ctx.accounts.collateral_vault, &ctx.accounts.user_collateral_account, + &ctx.accounts.collateral_vault, &ctx.accounts.collateral_vault_authority, - ctx.accounts.state.collateral_vault_nonce, - collateral_account_withdrawal, - )?; - - if insurance_account_withdrawal > 0 { - controller::token::send( - &ctx.accounts.token_program, - &ctx.accounts.insurance_vault, - &ctx.accounts.user_collateral_account, - &ctx.accounts.insurance_vault_authority, - ctx.accounts.state.insurance_vault_nonce, - insurance_account_withdrawal, - )?; - } - - let deposit_history = &mut ctx.accounts.deposit_history.load_mut()?; - let record_id = deposit_history.next_record_id(); - deposit_history.append(DepositRecord { - ts: now, - record_id, - user_authority: user.authority, - user: user.to_account_info().key(), - direction: DepositDirection::WITHDRAW, - collateral_before, - cumulative_deposits_before, - amount: amount_withdraw, - }); + &ctx.accounts.insurance_vault, + &ctx.accounts.insurance_vault_authority, + &ctx.accounts.funding_payment_history, + &ctx.accounts.deposit_history, + &Clock::get()?, + ) + } - Ok(()) + #[access_control( + exchange_not_paused(&ctx.accounts.state) + )] + pub fn transfer_collateral(ctx: Context, amount: u64) -> ProgramResult { + controller::deposits::transfer_collateral( + amount, + &mut ctx.accounts.from_user, + &mut ctx.accounts.from_user_positions, + &mut ctx.accounts.to_user, + &mut ctx.accounts.to_user_positions, + &ctx.accounts.markets, + &ctx.accounts.funding_payment_history, + &ctx.accounts.deposit_history, + &Clock::get()?, + ) } #[allow(unused_must_use)] @@ -1949,6 +1854,7 @@ pub mod clearing_house { &ctx.accounts.authority, ctx.remaining_accounts, optional_accounts, + 0, ) } @@ -1964,9 +1870,88 @@ pub mod clearing_house { &ctx.accounts.authority, ctx.remaining_accounts, optional_accounts, + 0, ) } + pub fn initialize_user_registry( + ctx: Context, + _user_registry_nonce: u8, + first_name: [u8; 32], + ) -> ProgramResult { + let user_registry = &mut ctx.accounts.user_registry; + + if !is_valid_name(first_name) { + return Err(print_error!(ErrorCode::InvalidUserName)().into()); + } + + user_registry.names = [UNINITIALIZED_NAME; 16]; + user_registry.names[0] = first_name; + user_registry.authority = ctx.accounts.authority.key(); + Ok(()) + } + + pub fn add_user( + ctx: Context, + user_seed: u8, + _user_nonce: u8, + name: [u8; 32], + ) -> ProgramResult { + let user_registry = &mut ctx.accounts.user_registry; + if user_registry.is_seed_taken(user_seed) { + msg!("Seed already taken {}", user_seed); + return Err(print_error!(ErrorCode::InvalidUserSeed)().into()); + } + + // can unwrap since we know user_seed isn't token + let next_available_seed = user_registry.next_available_seed().unwrap(); + if next_available_seed != user_seed { + msg!( + "User seed needs to be next available seed {}", + next_available_seed + ); + return Err(print_error!(ErrorCode::InvalidUserSeed)().into()); + } + + if !is_valid_name(name) { + return Err(print_error!(ErrorCode::InvalidUserName)().into()); + } + + user_registry.names[user_seed as usize] = name; + + user_initialization::initialize( + &ctx.accounts.state, + &mut ctx.accounts.user, + &ctx.accounts.user_positions, + &ctx.accounts.authority, + ctx.remaining_accounts, + InitializeUserOptionalAccounts { + whitelist_token: false, + }, + user_seed, + ) + } + + pub fn update_user_name( + ctx: Context, + user_seed: u8, + name: [u8; 32], + ) -> ProgramResult { + let user_registry = &mut ctx.accounts.user_registry; + if !user_registry.is_seed_taken(user_seed) { + msg!("Seed not in use {}", user_seed); + return Err(print_error!(ErrorCode::InvalidUserSeed)().into()); + } + + if !is_valid_name(name) { + return Err(print_error!(ErrorCode::InvalidUserName)().into()); + } + + user_registry.names[user_seed as usize] = name; + + Ok(()) + } + pub fn initialize_user_orders( ctx: Context, _user_orders_nonce: u8, diff --git a/programs/clearing_house/src/state/history/deposit.rs b/programs/clearing_house/src/state/history/deposit.rs index 73aa01e4..69f2b549 100644 --- a/programs/clearing_house/src/state/history/deposit.rs +++ b/programs/clearing_house/src/state/history/deposit.rs @@ -26,14 +26,16 @@ impl DepositHistory { #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq)] pub enum DepositDirection { - DEPOSIT, - WITHDRAW, + Deposit, + Withdraw, + TransferIn, + TransferOut, } impl Default for DepositDirection { // UpOnly fn default() -> Self { - DepositDirection::DEPOSIT + DepositDirection::Deposit } } diff --git a/programs/clearing_house/src/state/mod.rs b/programs/clearing_house/src/state/mod.rs index 8a951a33..5976b532 100644 --- a/programs/clearing_house/src/state/mod.rs +++ b/programs/clearing_house/src/state/mod.rs @@ -5,3 +5,4 @@ pub mod order_state; pub mod state; pub mod user; pub mod user_orders; +pub mod user_registry; diff --git a/programs/clearing_house/src/state/user.rs b/programs/clearing_house/src/state/user.rs index b5b0c4c5..834e284a 100644 --- a/programs/clearing_house/src/state/user.rs +++ b/programs/clearing_house/src/state/user.rs @@ -11,9 +11,10 @@ pub struct User { pub total_referral_reward: u128, pub total_referee_discount: u128, pub positions: Pubkey, + pub seed: u8, // upgrade-ability - pub padding0: u128, + pub padding0: [u8; 15], pub padding1: u128, pub padding2: u128, pub padding3: u128, diff --git a/programs/clearing_house/src/state/user_registry.rs b/programs/clearing_house/src/state/user_registry.rs new file mode 100644 index 00000000..1b14ab08 --- /dev/null +++ b/programs/clearing_house/src/state/user_registry.rs @@ -0,0 +1,27 @@ +use anchor_lang::prelude::*; + +pub const UNINITIALIZED_NAME: [u8; 32] = [32; 32]; + +#[account] +#[derive(Default)] +pub struct UserRegistry { + pub authority: Pubkey, + pub names: [[u8; 32]; 16], +} + +impl UserRegistry { + pub fn is_seed_taken(&self, seed: u8) -> bool { + self.names[seed as usize] != UNINITIALIZED_NAME + } + + pub fn next_available_seed(&self) -> Option { + self.names + .iter() + .position(|name| *name == UNINITIALIZED_NAME) + .map(|seed| seed as u8) + } +} + +pub fn is_valid_name(name: [u8; 32]) -> bool { + name != UNINITIALIZED_NAME +} diff --git a/programs/clearing_house/src/user_initialization.rs b/programs/clearing_house/src/user_initialization.rs index e121c37f..cfbc62fc 100644 --- a/programs/clearing_house/src/user_initialization.rs +++ b/programs/clearing_house/src/user_initialization.rs @@ -12,6 +12,7 @@ pub fn initialize( authority: &Signer, remaining_accounts: &[AccountInfo], optional_accounts: InitializeUserOptionalAccounts, + seed: u8, ) -> ProgramResult { if !state.whitelist_mint.eq(&Pubkey::default()) { let whitelist_token = @@ -35,8 +36,9 @@ pub fn initialize( user.collateral = 0; user.cumulative_deposits = 0; user.positions = *user_positions.to_account_info().key; + user.seed = seed; - user.padding0 = 0; + user.padding0 = [0; 15]; user.padding1 = 0; user.padding2 = 0; user.padding3 = 0; diff --git a/sdk/src/accounts/bulkUserSubscription.ts b/sdk/src/accounts/bulkUserSubscription.ts index 0a2525a7..58f8ddd0 100644 --- a/sdk/src/accounts/bulkUserSubscription.ts +++ b/sdk/src/accounts/bulkUserSubscription.ts @@ -32,36 +32,53 @@ export async function bulkPollingUserSubscribe( ]); // Create a map of the authority to keys - const authorityToKeys = new Map(); - const userToAuthority = new Map(); + const authorityToSeedToKeys = new Map>(); + const userToSeedAndAuthority = new Map(); for (const userProgramAccount of userProgramAccounts) { const userAccountPublicKey = userProgramAccount.publicKey; const userAccount = userProgramAccount.account as UserAccount; - authorityToKeys.set(userAccount.authority.toString(), { - user: userAccountPublicKey, - userPositions: userAccount.positions, - userOrders: undefined, - }); + if (authorityToSeedToKeys.has(userAccount.authority.toString())) { + const seedToKeys = authorityToSeedToKeys.get( + userAccount.authority.toString() + ); + seedToKeys.set(userAccount.seed, { + user: userAccountPublicKey, + userPositions: userAccount.positions, + userOrders: undefined, + }); + } else { + const seedToKeys = new Map(); + seedToKeys.set(userAccount.seed, { + user: userAccountPublicKey, + userPositions: userAccount.positions, + userOrders: undefined, + }); + authorityToSeedToKeys.set(userAccount.authority.toString(), seedToKeys); + } - userToAuthority.set( - userAccountPublicKey.toString(), - userAccount.authority.toString() - ); + userToSeedAndAuthority.set(userAccountPublicKey.toString(), [ + userAccount.seed, + userAccount.authority.toString(), + ]); } for (const orderProgramAccount of orderProgramAccounts) { const userOrderAccountPublicKey = orderProgramAccount.publicKey; const userOrderAccount = orderProgramAccount.account as UserOrdersAccount; - const authority = userToAuthority.get(userOrderAccount.user.toString()); - const userPublicKeys = authorityToKeys.get(authority); + const [seed, authority] = userToSeedAndAuthority.get( + userOrderAccount.user.toString() + ); + const userPublicKeys = authorityToSeedToKeys.get(authority).get(seed); userPublicKeys.userOrders = userOrderAccountPublicKey; } await Promise.all( users.map((user) => { // Pull the keys from the authority map so we can skip fetching them in addToAccountLoader - const userPublicKeys = authorityToKeys.get(user.authority.toString()); + const userPublicKeys = authorityToSeedToKeys + .get(user.authority.toString()) + .get(user.seed); return ( user.accountSubscriber as PollingUserAccountSubscriber ).addToAccountLoader(userPublicKeys); diff --git a/sdk/src/accounts/pollingUserAccountSubscriber.ts b/sdk/src/accounts/pollingUserAccountSubscriber.ts index 421bfeae..5189db51 100644 --- a/sdk/src/accounts/pollingUserAccountSubscriber.ts +++ b/sdk/src/accounts/pollingUserAccountSubscriber.ts @@ -23,6 +23,7 @@ export class PollingUserAccountSubscriber implements UserAccountSubscriber { program: Program; eventEmitter: StrictEventEmitter; authority: PublicKey; + seed: number; accountLoader: BulkAccountLoader; accountsToPoll = new Map(); @@ -37,13 +38,15 @@ export class PollingUserAccountSubscriber implements UserAccountSubscriber { public constructor( program: Program, authority: PublicKey, - accountLoader: BulkAccountLoader + accountLoader: BulkAccountLoader, + seed = 0 ) { this.isSubscribed = false; this.program = program; this.authority = authority; this.accountLoader = accountLoader; this.eventEmitter = new EventEmitter(); + this.seed = seed; } async subscribe(): Promise { @@ -67,7 +70,8 @@ export class PollingUserAccountSubscriber implements UserAccountSubscriber { if (!userPublicKeys) { const userPublicKey = await getUserAccountPublicKey( this.program.programId, - this.authority + this.authority, + this.seed ); const userAccount = (await this.program.account.user.fetch( diff --git a/sdk/src/accounts/types.ts b/sdk/src/accounts/types.ts index 706aa89c..24c7a56a 100644 --- a/sdk/src/accounts/types.ts +++ b/sdk/src/accounts/types.ts @@ -103,6 +103,7 @@ export interface UserAccountEvents { export interface UserAccountSubscriber { eventEmitter: StrictEventEmitter; isSubscribed: boolean; + seed: number; subscribe(): Promise; fetch(): Promise; diff --git a/sdk/src/accounts/webSocketUserAccountSubscriber.ts b/sdk/src/accounts/webSocketUserAccountSubscriber.ts index d943208b..10ad41ee 100644 --- a/sdk/src/accounts/webSocketUserAccountSubscriber.ts +++ b/sdk/src/accounts/webSocketUserAccountSubscriber.ts @@ -21,6 +21,7 @@ export class WebSocketUserAccountSubscriber implements UserAccountSubscriber { program: Program; eventEmitter: StrictEventEmitter; authority: PublicKey; + seed: number; userDataAccountSubscriber: AccountSubscriber; userPositionsAccountSubscriber: AccountSubscriber; @@ -28,11 +29,12 @@ export class WebSocketUserAccountSubscriber implements UserAccountSubscriber { type: ClearingHouseConfigType = 'websocket'; - public constructor(program: Program, authority: PublicKey) { + public constructor(program: Program, authority: PublicKey, seed = 0) { this.isSubscribed = false; this.program = program; this.authority = authority; this.eventEmitter = new EventEmitter(); + this.seed = seed; } async subscribe(): Promise { @@ -42,7 +44,8 @@ export class WebSocketUserAccountSubscriber implements UserAccountSubscriber { const userPublicKey = await getUserAccountPublicKey( this.program.programId, - this.authority + this.authority, + this.seed ); this.userDataAccountSubscriber = new WebSocketAccountSubscriber( 'user', diff --git a/sdk/src/addresses.ts b/sdk/src/addresses.ts index 2413b080..ffff83ad 100644 --- a/sdk/src/addresses.ts +++ b/sdk/src/addresses.ts @@ -33,19 +33,31 @@ export async function getClearingHouseStateAccountPublicKey( export async function getUserAccountPublicKeyAndNonce( programId: PublicKey, - authority: PublicKey + authority: PublicKey, + userSeed?: number ): Promise<[PublicKey, number]> { - return anchor.web3.PublicKey.findProgramAddress( - [Buffer.from(anchor.utils.bytes.utf8.encode('user')), authority.toBuffer()], - programId - ); + const seeds = + userSeed === undefined || userSeed === 0 + ? [ + Buffer.from(anchor.utils.bytes.utf8.encode('user')), + authority.toBuffer(), + ] + : [ + Buffer.from(anchor.utils.bytes.utf8.encode('user')), + Uint8Array.from([userSeed]), + authority.toBuffer(), + ]; + return anchor.web3.PublicKey.findProgramAddress(seeds, programId); } export async function getUserAccountPublicKey( programId: PublicKey, - authority: PublicKey + authority: PublicKey, + userSeed?: number ): Promise { - return (await getUserAccountPublicKeyAndNonce(programId, authority))[0]; + return ( + await getUserAccountPublicKeyAndNonce(programId, authority, userSeed) + )[0]; } export async function getUserOrdersAccountPublicKeyAndNonce( @@ -69,3 +81,23 @@ export async function getUserOrdersAccountPublicKey( await getUserOrdersAccountPublicKeyAndNonce(programId, userAccount) )[0]; } + +export async function getUserRegistryAccountPublicKey( + programId: PublicKey, + authority: PublicKey +): Promise { + return (await getUserRegistryPublicKeyAndNonce(programId, authority))[0]; +} + +export async function getUserRegistryPublicKeyAndNonce( + programId: PublicKey, + authority: PublicKey +): Promise<[PublicKey, number]> { + return anchor.web3.PublicKey.findProgramAddress( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('user_registry')), + authority.toBuffer(), + ], + programId + ); +} diff --git a/sdk/src/clearingHouse.ts b/sdk/src/clearingHouse.ts index 7c4cb28b..c03d1acf 100644 --- a/sdk/src/clearingHouse.ts +++ b/sdk/src/clearingHouse.ts @@ -21,6 +21,7 @@ import { OrderParams, Order, ExtendedCurveHistoryAccount, + UserRegistryAccount, } from './types'; import * as anchor from '@project-serum/anchor'; import clearingHouseIDL from './idl/clearing_house.json'; @@ -45,6 +46,8 @@ import { getUserAccountPublicKeyAndNonce, getUserOrdersAccountPublicKey, getUserOrdersAccountPublicKeyAndNonce, + getUserRegistryAccountPublicKey, + getUserRegistryPublicKeyAndNonce, } from './addresses'; import { ClearingHouseAccountSubscriber, @@ -58,6 +61,7 @@ import { getWebSocketClearingHouseConfig, } from './factory/clearingHouse'; import { ZERO } from './constants/numericConstants'; +import { UserRegistry } from './userRegistry/userRegistry'; /** * # ClearingHouse @@ -76,6 +80,7 @@ export class ClearingHouse { eventEmitter: StrictEventEmitter; _isSubscribed = false; txSender: TxSender; + seed: number; public get isSubscribed() { return this._isSubscribed && this.accountSubscriber.isSubscribed; @@ -91,19 +96,23 @@ export class ClearingHouse { * @param wallet * @param clearingHouseProgramId * @param opts + * @param seed * @returns */ public static from( connection: Connection, wallet: IWallet, clearingHouseProgramId: PublicKey, - opts: ConfirmOptions = Provider.defaultOptions() + opts: ConfirmOptions = Provider.defaultOptions(), + seed?: number ): ClearingHouse { const config = getWebSocketClearingHouseConfig( connection, wallet, clearingHouseProgramId, - opts + opts, + undefined, + seed ); return getClearingHouse(config); } @@ -114,7 +123,8 @@ export class ClearingHouse { program: Program, accountSubscriber: ClearingHouseAccountSubscriber, txSender: TxSender, - opts: ConfirmOptions + opts: ConfirmOptions, + seed = 0 ) { this.connection = connection; this.wallet = wallet; @@ -123,6 +133,7 @@ export class ClearingHouse { this.accountSubscriber = accountSubscriber; this.eventEmitter = this.accountSubscriber.eventEmitter; this.txSender = txSender; + this.seed = seed; } /** @@ -257,10 +268,18 @@ export class ClearingHouse { this.wallet = newWallet; this.provider = newProvider; this.program = newProgram; - this.userAccountPublicKey = undefined; - this.userAccount = undefined; - this.userOrdersAccountPublicKey = undefined; - this.userOrdersExist = undefined; + + // reset cached values + this.userAccountPublicKeyMap = new Map(); + this.userAccountMap = new Map(); + this.userOrdersAccountPublicKeyMap = new Map(); + this.userOrdersExistMap = new Map(); + this.userRegistryAccountPublicKey = undefined; + this.userRegistryAccount = undefined; + } + + public updateSeed(seed: number): void { + this.seed = seed; } public async initializeUserAccount(): Promise< @@ -371,64 +390,207 @@ export class ClearingHouse { ); } - userAccountPublicKey?: PublicKey; + public async initializeUserRegistryAccount( + firstUserName: number[] + ): Promise { + return this.txSender.send( + wrapInTx(await this.initializeUserRegistryAccountIx(firstUserName)), + [], + this.opts + ); + } + + public async initializeUserRegistryAccountIx( + firstUserName: number[] + ): Promise { + const [userRegistryAccountPublicKey, userRegistryAccountNonce] = + await getUserRegistryPublicKeyAndNonce( + this.program.programId, + this.wallet.publicKey + ); + + return await this.program.instruction.initializeUserRegistry( + userRegistryAccountNonce, + firstUserName, + { + accounts: { + userRegistry: userRegistryAccountPublicKey, + user: await this.getUserAccountPublicKey(), + authority: this.wallet.publicKey, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }, + } + ); + } + + public async addUser( + seed: number, + name: number[] + ): Promise { + const [addUserIx, userAccountPublicKey, userPositionsKeyPair] = + await this.addUserIx(seed, name); + const initializeUserOrderIx = await this.getInitializeUserOrdersInstruction( + userAccountPublicKey + ); + const tx = new Transaction().add(addUserIx).add(initializeUserOrderIx); + + return this.txSender.send(tx, [userPositionsKeyPair], this.opts); + } + + public async addUserIx( + seed: number, + name: number[] + ): Promise<[TransactionInstruction, PublicKey, Keypair]> { + const [userAccountPublicKey, userAccountNonce] = + await getUserAccountPublicKeyAndNonce( + this.program.programId, + this.wallet.publicKey, + seed + ); + + const userPositions = new Keypair(); + const ix = await this.program.instruction.addUser( + seed, + userAccountNonce, + name, + { + accounts: { + userRegistry: await this.getUserRegistryAccountPublicKey(), + user: userAccountPublicKey, + authority: this.wallet.publicKey, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + userPositions: userPositions.publicKey, + state: await this.getStatePublicKey(), + }, + } + ); + return [ix, userAccountPublicKey, userPositions]; + } + + public async updateUserName( + seed: number, + name: number[] + ): Promise { + return this.txSender.send( + wrapInTx(await this.updateUserNameIx(seed, name)), + [], + this.opts + ); + } + + public async updateUserNameIx( + seed: number, + name: number[] + ): Promise { + return await this.program.instruction.updateUserName(seed, name, { + accounts: { + userRegistry: await this.getUserRegistryAccountPublicKey(), + authority: this.wallet.publicKey, + }, + }); + } + + userAccountPublicKeyMap = new Map(); /** * Get the address for the Clearing House User's account. NOT the user's wallet address. * @returns */ public async getUserAccountPublicKey(): Promise { - if (this.userAccountPublicKey) { - return this.userAccountPublicKey; + if (this.userAccountPublicKeyMap.has(this.seed)) { + return this.userAccountPublicKeyMap.get(this.seed); } - this.userAccountPublicKey = await getUserAccountPublicKey( + const userAccountPublicKey = await getUserAccountPublicKey( this.program.programId, - this.wallet.publicKey + this.wallet.publicKey, + this.seed ); - return this.userAccountPublicKey; + this.userAccountPublicKeyMap.set(this.seed, userAccountPublicKey); + return userAccountPublicKey; } - userAccount?: UserAccount; + userAccountMap = new Map(); public async getUserAccount(): Promise { - if (this.userAccount) { - return this.userAccount; + if (this.userAccountMap.has(this.seed)) { + return this.userAccountMap.get(this.seed); } - this.userAccount = (await this.program.account.user.fetch( + const userAccount = (await this.program.account.user.fetch( await this.getUserAccountPublicKey() )) as UserAccount; - return this.userAccount; + this.userAccountMap.set(this.seed, userAccount); + return userAccount; } - userOrdersAccountPublicKey?: PublicKey; + userOrdersAccountPublicKeyMap = new Map(); /** * Get the address for the Clearing House User Order's account. NOT the user's wallet address. * @returns */ public async getUserOrdersAccountPublicKey(): Promise { - if (this.userOrdersAccountPublicKey) { - return this.userOrdersAccountPublicKey; + if (this.userOrdersAccountPublicKeyMap.has(this.seed)) { + return this.userOrdersAccountPublicKeyMap.get(this.seed); } - this.userOrdersAccountPublicKey = await getUserOrdersAccountPublicKey( + const userOrdersAccountPublicKey = await getUserOrdersAccountPublicKey( this.program.programId, await this.getUserAccountPublicKey() ); - return this.userOrdersAccountPublicKey; + this.userOrdersAccountPublicKeyMap.set( + this.seed, + userOrdersAccountPublicKey + ); + return userOrdersAccountPublicKey; } - userOrdersExist?: boolean; + userOrdersExistMap = new Map(); async userOrdersAccountExists(): Promise { - if (this.userOrdersExist) { - return this.userOrdersExist; + if (this.userOrdersExistMap.has(this.seed)) { + return this.userOrdersExistMap.get(this.seed); } const userOrdersAccountRPCResponse = await this.connection.getParsedAccountInfo( await this.getUserOrdersAccountPublicKey() ); - this.userOrdersExist = userOrdersAccountRPCResponse.value !== null; - return this.userOrdersExist; + const userOrdersExist = userOrdersAccountRPCResponse.value !== null; + this.userOrdersExistMap.set(this.seed, userOrdersExist); + return userOrdersExist; + } + + userRegistryAccountPublicKey?: PublicKey; + /** + * Get the address for the Clearing House User Registry account. + * @returns + */ + public async getUserRegistryAccountPublicKey(): Promise { + if (this.userRegistryAccountPublicKey) { + return this.userRegistryAccountPublicKey; + } + + this.userRegistryAccountPublicKey = await getUserRegistryAccountPublicKey( + this.program.programId, + this.wallet.publicKey + ); + return this.userRegistryAccountPublicKey; + } + + userRegistryAccount?: UserRegistryAccount; + public async getUserRegistryAccount(): Promise { + if (this.userRegistryAccount) { + return this.userRegistryAccount; + } + + this.userRegistryAccount = (await this.program.account.userRegistry.fetch( + await this.getUserRegistryAccountPublicKey() + )) as UserRegistryAccount; + return this.userRegistryAccount; + } + + public async getUserRegistry(): Promise { + return new UserRegistry(await this.getUserRegistryAccount()); } public async depositCollateral( @@ -597,6 +759,45 @@ export class ClearingHouse { }); } + public async transferCollateral( + amount: BN, + userPublicKey: PublicKey + ): Promise { + return this.txSender.send( + wrapInTx(await this.getTransferCollateralIx(amount, userPublicKey)), + [], + this.opts + ); + } + + public async getTransferCollateralIx( + amount: BN, + userPublicKey: PublicKey + ): Promise { + const fromUserAccountPublicKey = await this.getUserAccountPublicKey(); + const fromUser = (await this.program.account.user.fetch( + fromUserAccountPublicKey + )) as UserAccount; + const toUser = (await this.program.account.user.fetch( + userPublicKey + )) as UserAccount; + + const state = this.getStateAccount(); + return await this.program.instruction.transferCollateral(amount, { + accounts: { + state: await this.getStatePublicKey(), + fromUser: fromUserAccountPublicKey, + fromUserPositions: fromUser.positions, + toUser: userPublicKey, + toUserPositions: toUser.positions, + authority: this.wallet.publicKey, + markets: state.markets, + fundingPaymentHistory: state.fundingPaymentHistory, + depositHistory: state.depositHistory, + }, + }); + } + public async openPosition( direction: PositionDirection, amount: BN, diff --git a/sdk/src/clearingHouseUser.ts b/sdk/src/clearingHouseUser.ts index 4586bd17..449481be 100644 --- a/sdk/src/clearingHouseUser.ts +++ b/sdk/src/clearingHouseUser.ts @@ -48,6 +48,7 @@ export class ClearingHouseUser { userOrdersAccountPublicKey?: PublicKey; _isSubscribed = false; eventEmitter: StrictEventEmitter; + seed: number; public get isSubscribed() { return this._isSubscribed && this.accountSubscriber.isSubscribed; @@ -65,14 +66,16 @@ export class ClearingHouseUser { */ public static from( clearingHouse: ClearingHouse, - authority: PublicKey + authority: PublicKey, + seed?: number ): ClearingHouseUser { if (clearingHouse.accountSubscriber.type !== 'websocket') throw 'This method only works for clearing houses with a websocket account listener. Try using the getClearingHouseUser factory method to initialize a ClearingHouseUser instead'; const config = getWebSocketClearingHouseUserConfig( clearingHouse, - authority + authority, + seed ); return getClearingHouseUser(config); } @@ -80,12 +83,14 @@ export class ClearingHouseUser { public constructor( clearingHouse: ClearingHouse, authority: PublicKey, - accountSubscriber: UserAccountSubscriber + accountSubscriber: UserAccountSubscriber, + seed = 0 ) { this.clearingHouse = clearingHouse; this.authority = authority; this.accountSubscriber = accountSubscriber; this.eventEmitter = this.accountSubscriber.eventEmitter; + this.seed = seed; } /** @@ -172,7 +177,8 @@ export class ClearingHouseUser { this.userAccountPublicKey = await getUserAccountPublicKey( this.clearingHouse.program.programId, - this.authority + this.authority, + this.seed ); return this.userAccountPublicKey; } diff --git a/sdk/src/factory/clearingHouse.ts b/sdk/src/factory/clearingHouse.ts index 2fc72526..68cdbcbe 100644 --- a/sdk/src/factory/clearingHouse.ts +++ b/sdk/src/factory/clearingHouse.ts @@ -20,6 +20,7 @@ type BaseClearingHouseConfig = { programID: PublicKey; opts?: ConfirmOptions; txSender?: TxSender; + seed?: number; }; type WebSocketClearingHouseConfiguration = BaseClearingHouseConfig; @@ -28,7 +29,7 @@ type PollingClearingHouseConfiguration = BaseClearingHouseConfig & { accountLoader: BulkAccountLoader; }; -type ClearingHouseConfig = +export type ClearingHouseConfig = | PollingClearingHouseConfiguration | WebSocketClearingHouseConfiguration; @@ -37,7 +38,8 @@ export function getWebSocketClearingHouseConfig( wallet: IWallet, programID: PublicKey, opts: ConfirmOptions = Provider.defaultOptions(), - txSender?: TxSender + txSender?: TxSender, + seed?: number ): WebSocketClearingHouseConfiguration { return { type: 'websocket', @@ -46,6 +48,7 @@ export function getWebSocketClearingHouseConfig( programID, opts, txSender, + seed, }; } @@ -55,7 +58,8 @@ export function getPollingClearingHouseConfig( programID: PublicKey, accountLoader: BulkAccountLoader, opts: ConfirmOptions = Provider.defaultOptions(), - txSender?: TxSender + txSender?: TxSender, + seed?: number ): PollingClearingHouseConfiguration { return { type: 'polling', @@ -65,6 +69,7 @@ export function getPollingClearingHouseConfig( accountLoader, opts, txSender, + seed, }; } @@ -92,7 +97,8 @@ export function getClearingHouse(config: ClearingHouseConfig): ClearingHouse { program, accountSubscriber, txSender, - config.opts + config.opts, + config.seed ); } diff --git a/sdk/src/factory/clearingHouseUser.ts b/sdk/src/factory/clearingHouseUser.ts index e4b687f1..d1595090 100644 --- a/sdk/src/factory/clearingHouseUser.ts +++ b/sdk/src/factory/clearingHouseUser.ts @@ -12,6 +12,7 @@ type BaseClearingHouseUserConfig = { type: ClearingHouseUserConfigType; clearingHouse: ClearingHouse; authority: PublicKey; + seed?: number; }; type WebSocketClearingHouseUserConfig = BaseClearingHouseUserConfig; @@ -20,31 +21,35 @@ type PollingClearingHouseUserConfig = BaseClearingHouseUserConfig & { accountLoader: BulkAccountLoader; }; -type ClearingHouseUserConfig = +export type ClearingHouseUserConfig = | PollingClearingHouseUserConfig | WebSocketClearingHouseUserConfig; export function getWebSocketClearingHouseUserConfig( clearingHouse: ClearingHouse, - authority: PublicKey + authority: PublicKey, + seed?: number ): WebSocketClearingHouseUserConfig { return { type: 'websocket', clearingHouse, authority, + seed, }; } export function getPollingClearingHouseUserConfig( clearingHouse: ClearingHouse, authority: PublicKey, - accountLoader: BulkAccountLoader + accountLoader: BulkAccountLoader, + seed?: number ): PollingClearingHouseUserConfig { return { type: 'polling', clearingHouse, authority, accountLoader, + seed, }; } @@ -55,19 +60,22 @@ export function getClearingHouseUser( if (config.type === 'websocket') { accountSubscriber = new WebSocketUserAccountSubscriber( config.clearingHouse.program, - config.authority + config.authority, + config.seed ); } else if (config.type === 'polling') { accountSubscriber = new PollingUserAccountSubscriber( config.clearingHouse.program, config.authority, - (config as PollingClearingHouseUserConfig).accountLoader + (config as PollingClearingHouseUserConfig).accountLoader, + config.seed ); } return new ClearingHouseUser( config.clearingHouse, config.authority, - accountSubscriber + accountSubscriber, + config.seed ); } diff --git a/sdk/src/groups/clearingHouseGroup.ts b/sdk/src/groups/clearingHouseGroup.ts new file mode 100644 index 00000000..ec8e1815 --- /dev/null +++ b/sdk/src/groups/clearingHouseGroup.ts @@ -0,0 +1,26 @@ +import { UserRegistry } from '../userRegistry/userRegistry'; +import { ClearingHouse } from '../clearingHouse'; +import { + ClearingHouseConfig, + getClearingHouse, +} from '../factory/clearingHouse'; + +export class ClearingHouseGroup { + clearingHouses = new Array(); + seedMap = new Map(); + nameMap = new Map(); + + public constructor( + clearingHouseConfig: ClearingHouseConfig, + userRegistry: UserRegistry + ) { + for (const [seed, name] of userRegistry.getUserNames().entries()) { + clearingHouseConfig.seed = seed; + const clearingHouse = getClearingHouse(clearingHouseConfig); + + this.clearingHouses.push(clearingHouse); + this.seedMap.set(seed, clearingHouse); + this.nameMap.set(name, clearingHouse); + } + } +} diff --git a/sdk/src/groups/clearingHouseUserGroup.ts b/sdk/src/groups/clearingHouseUserGroup.ts new file mode 100644 index 00000000..9eb05425 --- /dev/null +++ b/sdk/src/groups/clearingHouseUserGroup.ts @@ -0,0 +1,26 @@ +import { + ClearingHouseUserConfig, + getClearingHouseUser, +} from '../factory/clearingHouseUser'; +import { UserRegistry } from '../userRegistry/userRegistry'; +import { ClearingHouseUser } from '../clearingHouseUser'; + +export class ClearingHouseUserGroup { + users = new Array(); + seedMap = new Map(); + nameMap = new Map(); + + public constructor( + clearingHouseUserConfig: ClearingHouseUserConfig, + userRegistry: UserRegistry + ) { + for (const [seed, name] of userRegistry.getUserNames().entries()) { + clearingHouseUserConfig.seed = seed; + const clearingHouseUser = getClearingHouseUser(clearingHouseUserConfig); + + this.users.push(clearingHouseUser); + this.seedMap.set(seed, clearingHouseUser); + this.nameMap.set(name, clearingHouseUser); + } + } +} diff --git a/sdk/src/idl/clearing_house.json b/sdk/src/idl/clearing_house.json index 7b9a4552..1ba8115c 100644 --- a/sdk/src/idl/clearing_house.json +++ b/sdk/src/idl/clearing_house.json @@ -237,7 +237,7 @@ "accounts": [ { "name": "state", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -298,7 +298,7 @@ "accounts": [ { "name": "state", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -369,6 +369,62 @@ } ] }, + { + "name": "transferCollateral", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "fromUser", + "isMut": true, + "isSigner": false + }, + { + "name": "fromUserPositions", + "isMut": true, + "isSigner": false + }, + { + "name": "toUser", + "isMut": true, + "isSigner": false + }, + { + "name": "toUserPositions", + "isMut": true, + "isSigner": false + }, + { + "name": "markets", + "isMut": false, + "isSigner": false + }, + { + "name": "fundingPaymentHistory", + "isMut": true, + "isSigner": false + }, + { + "name": "depositHistory", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, { "name": "openPosition", "accounts": [ @@ -1344,6 +1400,140 @@ } ] }, + { + "name": "initializeUserRegistry", + "accounts": [ + { + "name": "userRegistry", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "user", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "userRegistryNonce", + "type": "u8" + }, + { + "name": "firstName", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "addUser", + "accounts": [ + { + "name": "userRegistry", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "userPositions", + "isMut": true, + "isSigner": true + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "userSeed", + "type": "u8" + }, + { + "name": "userNonce", + "type": "u8" + }, + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "updateUserName", + "accounts": [ + { + "name": "userRegistry", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + } + ], + "args": [ + { + "name": "userSeed", + "type": "u8" + }, + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, { "name": "initializeUserOrders", "accounts": [ @@ -2501,9 +2691,18 @@ "name": "positions", "type": "publicKey" }, + { + "name": "seed", + "type": "u8" + }, { "name": "padding0", - "type": "u128" + "type": { + "array": [ + "u8", + 15 + ] + } }, { "name": "padding1", @@ -2565,6 +2764,32 @@ } ] } + }, + { + "name": "UserRegistry", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "publicKey" + }, + { + "name": "names", + "type": { + "array": [ + { + "array": [ + "u8", + 32 + ] + }, + 16 + ] + } + } + ] + } } ], "types": [ @@ -3799,10 +4024,16 @@ "kind": "enum", "variants": [ { - "name": "DEPOSIT" + "name": "Deposit" + }, + { + "name": "Withdraw" }, { - "name": "WITHDRAW" + "name": "TransferIn" + }, + { + "name": "TransferOut" } ] } @@ -4249,6 +4480,16 @@ "code": 6060, "name": "CantExpireOrders", "msg": "CantExpireOrders" + }, + { + "code": 6061, + "name": "InvalidUserName", + "msg": "InvalidUserName" + }, + { + "code": 6062, + "name": "InvalidUserSeed", + "msg": "UserSeedAlreadyUsed" } ] } \ No newline at end of file diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 6886e606..8b30400a 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -12,6 +12,13 @@ export class PositionDirection { static readonly SHORT = { short: {} }; } +export class DepositDirection { + static readonly DEPOSIT = { deposit: {} }; + static readonly WITHDRAW = { withdraw: {} }; + static readonly TRANSFER_IN = { transferIn: {} }; + static readonly TRANSFER_OUT = { transferOut: {} }; +} + export class OracleSource { static readonly PYTH = { pyth: {} }; static readonly SWITCHBOARD = { switchboard: {} }; @@ -111,10 +118,7 @@ export type DepositRecord = { recordId: BN; userAuthority: PublicKey; user: PublicKey; - direction: { - deposit?: any; - withdraw?: any; - }; + direction: DepositDirection; collateralBefore: BN; cumulativeDepositsBefore: BN; amount: BN; @@ -334,6 +338,12 @@ export type UserAccount = { totalTokenDiscount: BN; totalReferralReward: BN; totalRefereeDiscount: BN; + seed: number; +}; + +export type UserRegistryAccount = { + authority: PublicKey; + names: Array>; }; export type UserOrdersAccount = { diff --git a/sdk/src/userRegistry/userName.ts b/sdk/src/userRegistry/userName.ts new file mode 100644 index 00000000..83179b8d --- /dev/null +++ b/sdk/src/userRegistry/userName.ts @@ -0,0 +1,18 @@ +export const MAX_NAME_LENGTH = 32; + +export function encodeName(name: string): number[] { + if (name.length > MAX_NAME_LENGTH) { + throw Error(`${name} longer than 32 characters`); + } + + const buffer = Buffer.alloc(32); + buffer.fill(name); + buffer.fill(' ', name.length); + + return Array(...buffer); +} + +export function decodeName(bytes: number[]): string { + const buffer = Buffer.from(bytes); + return buffer.toString('utf8').trim(); +} diff --git a/sdk/src/userRegistry/userRegistry.ts b/sdk/src/userRegistry/userRegistry.ts new file mode 100644 index 00000000..0e98f37d --- /dev/null +++ b/sdk/src/userRegistry/userRegistry.ts @@ -0,0 +1,24 @@ +import { UserRegistryAccount } from '../types'; +import { decodeName } from './userName'; + +export class UserRegistry { + account: UserRegistryAccount; + names: string[]; + + public constructor(userRegistryAccount: UserRegistryAccount) { + this.account = userRegistryAccount; + this.names = this.account.names.map((name) => decodeName(name)); + } + + public nextAvailableSeed(): number | undefined { + return this.names.findIndex((name) => name !== ''); + } + + /** + * Gives the set of names that have been registered by authority. The index of the name is the corresponding seed + * for a UserAccount + */ + public getUserNames(): string[] { + return this.names.filter((name) => name !== ''); + } +} diff --git a/test-scripts/run-anchor-tests.sh b/test-scripts/run-anchor-tests.sh index fcc99d7a..44408362 100644 --- a/test-scripts/run-anchor-tests.sh +++ b/test-scripts/run-anchor-tests.sh @@ -4,7 +4,7 @@ if [ "$1" != "--skip-build" ] cp target/idl/clearing_house.json sdk/src/idl/ fi -test_files=(order.ts orderReferrer.ts marketOrder.ts triggerOrders.ts stopLimits.ts userOrderId.ts makerOrder.ts roundInFavorBaseAsset.ts marketOrderBaseAssetAmount.ts expireOrders oracleOffsetOrders.ts clearingHouse.ts pyth.ts userAccount.ts admin.ts updateK.ts adminWithdraw.ts curve.ts whitelist.ts fees.ts idempotentCurve.ts maxDeposit.ts deleteUser.ts maxPositions.ts maxReserves.ts twapDivergenceLiquidation.ts oraclePnlLiquidation.ts whaleLiquidation.ts roundInFavor.ts minimumTradeSize.ts cappedSymFunding.ts) +test_files=(userRegistry.ts order.ts orderReferrer.ts marketOrder.ts triggerOrders.ts stopLimits.ts userOrderId.ts makerOrder.ts roundInFavorBaseAsset.ts marketOrderBaseAssetAmount.ts expireOrders oracleOffsetOrders.ts clearingHouse.ts pyth.ts userAccount.ts admin.ts updateK.ts adminWithdraw.ts curve.ts whitelist.ts fees.ts idempotentCurve.ts maxDeposit.ts deleteUser.ts maxPositions.ts maxReserves.ts twapDivergenceLiquidation.ts oraclePnlLiquidation.ts whaleLiquidation.ts roundInFavor.ts minimumTradeSize.ts cappedSymFunding.ts) for test_file in ${test_files[@]}; do export ANCHOR_TEST_FILE=${test_file} && anchor test --skip-build || exit 1; diff --git a/tests/userRegistry.ts b/tests/userRegistry.ts new file mode 100644 index 00000000..d52813b0 --- /dev/null +++ b/tests/userRegistry.ts @@ -0,0 +1,278 @@ +import * as anchor from '@project-serum/anchor'; +import { assert } from 'chai'; + +import { Program } from '@project-serum/anchor'; + +import { + Admin, + BN, + ClearingHouseUser, + UserRegistryAccount, + getUserAccountPublicKey, + UserAccount, + getWebSocketClearingHouseUserConfig, +} from '../sdk/src'; + +import { mockUSDCMint, mockUserUSDCAccount } from './testHelpers'; +import { decodeName, encodeName } from '../sdk/src/userRegistry/userName'; +import { + getUserOrdersAccountPublicKey, + getWebSocketClearingHouseConfig, + UserOrdersAccount, + ZERO, +} from '../sdk'; +import { ClearingHouseUserGroup } from '../sdk/src/groups/clearingHouseUserGroup'; +import { ClearingHouseGroup } from '../sdk/src/groups/clearingHouseGroup'; + +describe('user registry', () => { + const provider = anchor.Provider.local(undefined, { + commitment: 'confirmed', + preflightCommitment: 'confirmed', + }); + const connection = provider.connection; + anchor.setProvider(provider); + const chProgram = anchor.workspace.ClearingHouse as Program; + + let clearingHouse: Admin; + + let usdcMint; + let userUSDCAccount; + + const usdcAmount = new BN(10 * 10 ** 6); + + before(async () => { + usdcMint = await mockUSDCMint(provider); + userUSDCAccount = await mockUserUSDCAccount(usdcMint, usdcAmount, provider); + + clearingHouse = Admin.from( + connection, + provider.wallet, + chProgram.programId, + { + commitment: 'confirmed', + } + ); + await clearingHouse.initialize(usdcMint.publicKey, true); + await clearingHouse.subscribe(['depositHistoryAccount']); + + await clearingHouse.initializeUserAccountAndDepositCollateral( + usdcAmount, + userUSDCAccount.publicKey + ); + }); + + after(async () => { + await clearingHouse.unsubscribe(); + }); + + it('fail init user registry', async () => { + const invalidName = Array(...Buffer.alloc(32).fill(' ')); + try { + await clearingHouse.initializeUserRegistryAccount(invalidName); + } catch (e) { + return; + } + assert(false); + }); + + it('init user registry', async () => { + const name = 'crisp'; + const encodedName = encodeName(name); + await clearingHouse.initializeUserRegistryAccount(encodedName); + + const registry = (await clearingHouse.program.account.userRegistry.fetch( + await clearingHouse.getUserRegistryAccountPublicKey() + )) as UserRegistryAccount; + + assert(registry.authority.equals(provider.wallet.publicKey)); + const decodedName = decodeName(registry.names[0]); + assert(name === decodedName); + }); + + it('add user', async () => { + const name = 'crisp1'; + const encodedName = encodeName(name); + const seed = 1; + await clearingHouse.addUser(seed, encodedName); + + const registry = (await clearingHouse.program.account.userRegistry.fetch( + await clearingHouse.getUserRegistryAccountPublicKey() + )) as UserRegistryAccount; + + assert(registry.authority.equals(provider.wallet.publicKey)); + const decodedName = decodeName(registry.names[1]); + assert(name === decodedName); + + const secondUserAccountPublicKey = await getUserAccountPublicKey( + clearingHouse.program.programId, + provider.wallet.publicKey, + seed + ); + + const secondUserAccount = (await clearingHouse.program.account.user.fetch( + secondUserAccountPublicKey + )) as UserAccount; + assert(secondUserAccount.seed === 1); + + const secondUserOrdersAccountPublicKey = + await getUserOrdersAccountPublicKey( + clearingHouse.program.programId, + secondUserAccountPublicKey + ); + + const secondUserOrdersAccount = + (await clearingHouse.program.account.userOrders.fetch( + secondUserOrdersAccountPublicKey + )) as UserOrdersAccount; + + assert(secondUserOrdersAccount !== undefined); + }); + + it('fail add user', async () => { + const name = 'crisp1'; + const encodedName = encodeName(name); + const seed = 1; + try { + await clearingHouse.addUser(seed, encodedName); + } catch (e) { + return; + } + assert(false); + }); + + it('update user name', async () => { + const name = 'lil perp'; + const encodedName = encodeName(name); + const seed = 1; + await clearingHouse.updateUserName(seed, encodedName); + + const registry = (await clearingHouse.program.account.userRegistry.fetch( + await clearingHouse.getUserRegistryAccountPublicKey() + )) as UserRegistryAccount; + + const decodedName = decodeName(registry.names[1]); + assert(name === decodedName); + }); + + it('transfer collateral', async () => { + const toClearingHouseUser = ClearingHouseUser.from( + clearingHouse, + provider.wallet.publicKey, + 1 + ); + await toClearingHouseUser.subscribe(); + + await clearingHouse.transferCollateral( + usdcAmount, + await toClearingHouseUser.getUserAccountPublicKey() + ); + + const toUserAccount = toClearingHouseUser.getUserAccount(); + + assert(toUserAccount.collateral.eq(usdcAmount)); + assert(toUserAccount.cumulativeDeposits.eq(usdcAmount)); + + const fromUserAccount = (await clearingHouse.program.account.user.fetch( + await clearingHouse.getUserAccountPublicKey() + )) as UserAccount; + + assert(fromUserAccount.collateral.eq(ZERO)); + assert(fromUserAccount.cumulativeDeposits.eq(ZERO)); + + const depositsHistory = clearingHouse.getDepositHistoryAccount(); + + const transferOutRecord = depositsHistory.depositRecords[1]; + assert(transferOutRecord.direction.hasOwnProperty('transferOut')); + assert(transferOutRecord.userAuthority.equals(provider.wallet.publicKey)); + assert( + transferOutRecord.user.equals( + await clearingHouse.getUserAccountPublicKey() + ) + ); + assert(transferOutRecord.collateralBefore.eq(usdcAmount)); + assert(transferOutRecord.cumulativeDepositsBefore.eq(usdcAmount)); + assert(transferOutRecord.amount.eq(usdcAmount)); + + const transferInRecord = depositsHistory.depositRecords[2]; + assert(transferInRecord.direction.hasOwnProperty('transferIn')); + assert(transferInRecord.userAuthority.equals(provider.wallet.publicKey)); + assert( + transferInRecord.user.equals( + await toClearingHouseUser.getUserAccountPublicKey() + ) + ); + assert(transferInRecord.collateralBefore.eq(ZERO)); + assert(transferInRecord.cumulativeDepositsBefore.eq(ZERO)); + assert(transferOutRecord.amount.eq(usdcAmount)); + + await toClearingHouseUser.unsubscribe(); + }); + + it('test user groups', async () => { + const userRegistry = await clearingHouse.getUserRegistry(); + const userConfig = getWebSocketClearingHouseUserConfig( + // @ts-ignore + clearingHouse, + provider.wallet.publicKey + ); + + const userGroup = new ClearingHouseUserGroup(userConfig, userRegistry); + await Promise.all(userGroup.users.map((user) => user.subscribe())); + + for (const [seed, user] of userGroup.users.entries()) { + const expectedUserAccountPublicKey = await getUserAccountPublicKey( + clearingHouse.program.programId, + provider.wallet.publicKey, + seed + ); + assert( + expectedUserAccountPublicKey.equals( + await user.getUserAccountPublicKey() + ) + ); + } + + await Promise.all(userGroup.users.map((user) => user.unsubscribe())); + }); + + it('test clearing house groups', async () => { + const userRegistry = await clearingHouse.getUserRegistry(); + const clearingHouseConfig = getWebSocketClearingHouseConfig( + connection, + provider.wallet, + chProgram.programId + ); + + const clearingHouseGroup = new ClearingHouseGroup( + clearingHouseConfig, + userRegistry + ); + await Promise.all( + clearingHouseGroup.clearingHouses.map((clearingHouse) => + clearingHouse.subscribe() + ) + ); + + for (const [ + seed, + clearingHouse, + ] of clearingHouseGroup.clearingHouses.entries()) { + const expectedUserAccountPublicKey = await getUserAccountPublicKey( + clearingHouse.program.programId, + provider.wallet.publicKey, + seed + ); + assert( + expectedUserAccountPublicKey.equals( + await clearingHouse.getUserAccountPublicKey() + ) + ); + } + + await Promise.all( + clearingHouseGroup.clearingHouses.map((clearingHouse) => + clearingHouse.unsubscribe() + ) + ); + }); +});