diff --git a/Cargo.lock b/Cargo.lock index 25685a4..f109f82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -561,6 +561,8 @@ dependencies = [ "beethoven-swap-aldrin-v2", "beethoven-swap-futarchy", "beethoven-swap-gamma", + "beethoven-swap-goonfi", + "beethoven-swap-goonfi-v2", "beethoven-swap-heaven", "beethoven-swap-manifest", "beethoven-swap-perena", @@ -576,7 +578,6 @@ dependencies = [ "solana-account-view", "solana-address 2.0.0", "solana-clock", - "solana-compute-budget-instruction", "solana-instruction", "solana-instruction-view", "solana-keypair", @@ -680,6 +681,28 @@ dependencies = [ "solana-program-error", ] +[[package]] +name = "beethoven-swap-goonfi" +version = "0.0.1" +dependencies = [ + "beethoven-core", + "solana-account-view", + "solana-address 2.0.0", + "solana-instruction-view", + "solana-program-error", +] + +[[package]] +name = "beethoven-swap-goonfi-v2" +version = "0.0.1" +dependencies = [ + "beethoven-core", + "solana-account-view", + "solana-address 2.0.0", + "solana-instruction-view", + "solana-program-error", +] + [[package]] name = "beethoven-swap-heaven" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 8c71c2e..588086f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,8 @@ swap = [ "perena-swap", "solfi-swap", "solfi_v2-swap", + "goonfi-swap", + "goonfi_v2-swap", "manifest-swap", "heaven-swap", "aldrin-swap", @@ -35,6 +37,8 @@ jupiter-deposit = ["dep:beethoven-deposit-jupiter"] perena-swap = ["dep:beethoven-swap-perena"] solfi-swap = ["dep:beethoven-swap-solfi"] solfi_v2-swap = ["dep:beethoven-swap-solfi-v2"] +goonfi-swap = ["dep:beethoven-swap-goonfi"] +goonfi_v2-swap = ["dep:beethoven-swap-goonfi-v2"] manifest-swap = ["dep:beethoven-swap-manifest"] heaven-swap = ["dep:beethoven-swap-heaven"] aldrin-swap = ["dep:beethoven-swap-aldrin"] @@ -57,6 +61,8 @@ beethoven-deposit-jupiter = { path = "crates/deposit/jupiter", optional = true } beethoven-swap-perena = { path = "crates/swap/perena", optional = true } beethoven-swap-solfi = { path = "crates/swap/solfi", optional = true } beethoven-swap-solfi-v2 = { path = "crates/swap/solfi-v2", optional = true } +beethoven-swap-goonfi = { path = "crates/swap/goonfi", optional = true } +beethoven-swap-goonfi-v2 = { path = "crates/swap/goonfi-v2", optional = true } beethoven-swap-manifest = { path = "crates/swap/manifest", optional = true } beethoven-swap-heaven = { path = "crates/swap/heaven", optional = true } beethoven-swap-aldrin = { path = "crates/swap/aldrin", optional = true } @@ -74,6 +80,8 @@ members = [ "crates/swap/perena", "crates/swap/solfi", "crates/swap/solfi-v2", + "crates/swap/goonfi", + "crates/swap/goonfi-v2", "crates/swap/manifest", "crates/swap/heaven", "crates/swap/aldrin", @@ -100,7 +108,6 @@ solana-rent = "3.1.0" solana-sdk-ids = "3.1.0" solana-signer = "3.0.0" solana-transaction = "3.0.2" -solana-compute-budget-instruction = "3.1.7" spl-token-interface = "2" mollusk-svm = "0.10.1" mollusk-svm-programs-token = "0.10.1" diff --git a/crates/swap/goonfi-v2/Cargo.toml b/crates/swap/goonfi-v2/Cargo.toml new file mode 100644 index 0000000..082e312 --- /dev/null +++ b/crates/swap/goonfi-v2/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "beethoven-swap-goonfi-v2" +description = "Goonfi V2 DEX swap implementation for Beethoven" +version = "0.0.1" +license = "MIT" +edition = "2021" + + +[dependencies] +beethoven-core = { path = "../../core" } +solana-account-view = "1.0.0" +solana-address = { version = "2.0.0", features = ["decode"] } +solana-instruction-view = "1.0.0" +solana-program-error = "3.0.0" diff --git a/crates/swap/goonfi-v2/src/lib.rs b/crates/swap/goonfi-v2/src/lib.rs new file mode 100644 index 0000000..4045307 --- /dev/null +++ b/crates/swap/goonfi-v2/src/lib.rs @@ -0,0 +1,166 @@ +#![no_std] + +use { + beethoven_core::Swap, + core::mem::MaybeUninit, + solana_account_view::AccountView, + solana_address::Address, + solana_instruction_view::{ + cpi::{invoke_signed, Signer}, + InstructionAccount, InstructionView, + }, + solana_program_error::{ProgramError, ProgramResult}, +}; + +pub const GOONFI_V2_PROGRAM_ID: Address = + Address::from_str_const("goonuddtQRrWqqn5nFyczVKaie28f3kDkHWkHtURSLE"); + +const SWAP_DISCRIMINATOR: u8 = 1; + +pub struct GoonfiV2; + +pub struct GoonfiV2SwapData { + pub is_bid: bool, + pub is_ultra: bool, +} + +impl TryFrom<&[u8]> for GoonfiV2SwapData { + type Error = ProgramError; + + fn try_from(data: &[u8]) -> Result { + if data.len() < 2 { + return Err(ProgramError::InvalidInstructionData); + } + + Ok(Self { + is_bid: data[0] != 0, + is_ultra: data[1] != 0, + }) + } +} + +pub struct GoonfiV2SwapAccounts<'info> { + pub goonfi_v2_program: &'info AccountView, + pub payer: &'info AccountView, + pub market: &'info AccountView, + pub base_token_account: &'info AccountView, + pub quote_token_account: &'info AccountView, + pub base_vault: &'info AccountView, + pub quote_vault: &'info AccountView, + pub base_mint: &'info AccountView, + pub quote_mint: &'info AccountView, + pub oracle: &'info AccountView, + pub blacklist: &'info AccountView, + pub instructions_sysvar: &'info AccountView, + pub base_token_program: &'info AccountView, + pub quote_token_program: &'info AccountView, +} + +impl<'info> TryFrom<&'info [AccountView]> for GoonfiV2SwapAccounts<'info> { + type Error = ProgramError; + + fn try_from(accounts: &'info [AccountView]) -> Result { + if accounts.len() < 14 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + let [goonfi_v2_program, payer, market, base_token_account, quote_token_account, base_vault, quote_vault, base_mint, quote_mint, oracle, blacklist, instructions_sysvar, base_token_program, quote_token_program, ..] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + Ok(Self { + goonfi_v2_program, + payer, + market, + base_token_account, + quote_token_account, + base_vault, + quote_vault, + base_mint, + quote_mint, + oracle, + blacklist, + instructions_sysvar, + base_token_program, + quote_token_program, + }) + } +} + +impl<'info> Swap<'info> for GoonfiV2 { + type Accounts = GoonfiV2SwapAccounts<'info>; + type Data = GoonfiV2SwapData; + + fn swap_signed( + ctx: &Self::Accounts, + in_amount: u64, + minimum_out_amount: u64, + data: &Self::Data, + signer_seeds: &[Signer], + ) -> ProgramResult { + let accounts = [ + InstructionAccount::readonly_signer(ctx.payer.address()), + InstructionAccount::writable(ctx.market.address()), + InstructionAccount::writable(ctx.base_token_account.address()), + InstructionAccount::writable(ctx.quote_token_account.address()), + InstructionAccount::writable(ctx.base_vault.address()), + InstructionAccount::writable(ctx.quote_vault.address()), + InstructionAccount::readonly(ctx.base_mint.address()), + InstructionAccount::readonly(ctx.quote_mint.address()), + InstructionAccount::readonly(ctx.oracle.address()), + InstructionAccount::readonly(ctx.blacklist.address()), + InstructionAccount::readonly(ctx.instructions_sysvar.address()), + InstructionAccount::readonly(ctx.base_token_program.address()), + InstructionAccount::readonly(ctx.quote_token_program.address()), + ]; + + let account_infos = [ + ctx.payer, + ctx.market, + ctx.base_token_account, + ctx.quote_token_account, + ctx.base_vault, + ctx.quote_vault, + ctx.base_mint, + ctx.quote_mint, + ctx.oracle, + ctx.blacklist, + ctx.instructions_sysvar, + ctx.base_token_program, + ctx.quote_token_program, + ]; + + let mut instruction_data = MaybeUninit::<[u8; 19]>::uninit(); + unsafe { + let ptr = instruction_data.as_mut_ptr() as *mut u8; + core::ptr::write(ptr, SWAP_DISCRIMINATOR); + core::ptr::write(ptr.add(1), data.is_bid as u8); + core::ptr::copy_nonoverlapping(in_amount.to_le_bytes().as_ptr(), ptr.add(2), 8); + core::ptr::copy_nonoverlapping( + minimum_out_amount.to_le_bytes().as_ptr(), + ptr.add(10), + 8, + ); + core::ptr::write(ptr.add(18), data.is_ultra as u8); + } + + let instruction = InstructionView { + program_id: &GOONFI_V2_PROGRAM_ID, + accounts: &accounts, + data: unsafe { instruction_data.assume_init_ref() }, + }; + + invoke_signed(&instruction, &account_infos, signer_seeds) + } + + fn swap( + ctx: &Self::Accounts, + in_amount: u64, + minimum_out_amount: u64, + data: &Self::Data, + ) -> ProgramResult { + Self::swap_signed(ctx, in_amount, minimum_out_amount, data, &[]) + } +} diff --git a/crates/swap/goonfi/Cargo.toml b/crates/swap/goonfi/Cargo.toml new file mode 100644 index 0000000..c2c7427 --- /dev/null +++ b/crates/swap/goonfi/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "beethoven-swap-goonfi" +description = "Goonfi DEX swap implementation for Beethoven" +version = "0.0.1" +license = "MIT" +edition = "2021" + + +[dependencies] +beethoven-core = { path = "../../core" } +solana-account-view = "1.0.0" +solana-address = { version = "2.0.0", features = ["decode"] } +solana-instruction-view = "1.0.0" +solana-program-error = "3.0.0" diff --git a/crates/swap/goonfi/src/lib.rs b/crates/swap/goonfi/src/lib.rs new file mode 100644 index 0000000..1d1e32c --- /dev/null +++ b/crates/swap/goonfi/src/lib.rs @@ -0,0 +1,155 @@ +#![no_std] + +use { + beethoven_core::Swap, + core::mem::MaybeUninit, + solana_account_view::AccountView, + solana_address::Address, + solana_instruction_view::{ + cpi::{invoke_signed, Signer}, + InstructionAccount, InstructionView, + }, + solana_program_error::{ProgramError, ProgramResult}, +}; + +pub const GOONFI_PROGRAM_ID: Address = + Address::from_str_const("goonERTdGsjnkZqWuVjs73BZ3Pb9qoCUdBUL17BnS5j"); + +const SWAP_DISCRIMINATOR: u8 = 2; + +pub struct Goonfi; + +pub struct GoonfiSwapData { + pub is_bid: bool, + pub is_ultra: bool, + pub bump: u8, +} + +impl TryFrom<&[u8]> for GoonfiSwapData { + type Error = ProgramError; + + fn try_from(data: &[u8]) -> Result { + if data.len() < 20 { + return Err(ProgramError::InvalidInstructionData); + } + + Ok(Self { + is_bid: data[1] != 0, + is_ultra: data[19] != 0, + bump: data[2], + }) + } +} + +pub struct GoonfiSwapAccounts<'info> { + pub goonfi_program: &'info AccountView, + pub payer: &'info AccountView, + pub market: &'info AccountView, + pub base_token_account: &'info AccountView, + pub quote_token_account: &'info AccountView, + pub base_vault: &'info AccountView, + pub quote_vault: &'info AccountView, + pub blacklist: &'info AccountView, + pub instructions_sysvar: &'info AccountView, + pub token_program: &'info AccountView, +} + +pub const GOONFI_SWAP_IX_ACCOUNTS_LEN: usize = 9; + +impl<'info> TryFrom<&'info [AccountView]> for GoonfiSwapAccounts<'info> { + type Error = ProgramError; + + fn try_from(accounts: &'info [AccountView]) -> Result { + if accounts.len() < 10 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + let [goonfi_program, payer, market, base_token_account, quote_token_account, base_vault, quote_vault, blacklist, instructions_sysvar, token_program, ..] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + Ok(Self { + goonfi_program, + payer, + market, + base_token_account, + quote_token_account, + base_vault, + quote_vault, + blacklist, + instructions_sysvar, + token_program, + }) + } +} + +impl<'info> Swap<'info> for Goonfi { + type Accounts = GoonfiSwapAccounts<'info>; + type Data = GoonfiSwapData; + + fn swap_signed( + ctx: &Self::Accounts, + in_amount: u64, + minimum_out_amount: u64, + data: &Self::Data, + signer_seeds: &[Signer], + ) -> ProgramResult { + let accounts = [ + InstructionAccount::readonly_signer(ctx.payer.address()), + InstructionAccount::writable(ctx.market.address()), + InstructionAccount::writable(ctx.base_token_account.address()), + InstructionAccount::writable(ctx.quote_token_account.address()), + InstructionAccount::writable(ctx.base_vault.address()), + InstructionAccount::writable(ctx.quote_vault.address()), + InstructionAccount::readonly(ctx.blacklist.address()), + InstructionAccount::readonly(ctx.instructions_sysvar.address()), + InstructionAccount::readonly(ctx.token_program.address()), + ]; + + let account_infos = [ + ctx.payer, + ctx.market, + ctx.base_token_account, + ctx.quote_token_account, + ctx.base_vault, + ctx.quote_vault, + ctx.blacklist, + ctx.instructions_sysvar, + ctx.token_program, + ]; + + let mut instruction_data = MaybeUninit::<[u8; 20]>::uninit(); + unsafe { + let ptr = instruction_data.as_mut_ptr() as *mut u8; + core::ptr::write(ptr, SWAP_DISCRIMINATOR); + core::ptr::write(ptr.add(1), data.is_bid as u8); + core::ptr::write(ptr.add(2), data.bump); + core::ptr::copy_nonoverlapping(in_amount.to_le_bytes().as_ptr(), ptr.add(3), 8); + core::ptr::copy_nonoverlapping( + minimum_out_amount.to_le_bytes().as_ptr(), + ptr.add(11), + 8, + ); + core::ptr::write(ptr.add(19), data.is_ultra as u8); + } + + let instruction = InstructionView { + program_id: &GOONFI_PROGRAM_ID, + accounts: &accounts, + data: unsafe { instruction_data.assume_init_ref() }, + }; + + invoke_signed(&instruction, &account_infos, signer_seeds) + } + + fn swap( + ctx: &Self::Accounts, + in_amount: u64, + minimum_out_amount: u64, + data: &Self::Data, + ) -> ProgramResult { + Self::swap_signed(ctx, in_amount, minimum_out_amount, data, &[]) + } +}