Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ swap = [
"omnipair-swap",
"hadron-swap",
"raydium-cpmm-swap",
"unitas-vault-swap",
]

# Deposit protocols
Expand All @@ -51,6 +52,7 @@ scale_vmm-swap = ["dep:beethoven-swap-scale-vmm"]
omnipair-swap = ["dep:beethoven-swap-omnipair"]
hadron-swap = ["dep:beethoven-swap-hadron"]
raydium-cpmm-swap = ["dep:beethoven-swap-raydium-cpmm"]
unitas-vault-swap = ["dep:beethoven-swap-unitas-vault"]

[dependencies]
beethoven-core = { path = "crates/core" }
Expand Down Expand Up @@ -78,6 +80,7 @@ beethoven-swap-scale-vmm = { path = "crates/swap/scale-vmm", optional = true }
beethoven-swap-omnipair = { path = "crates/swap/omnipair", optional = true }
beethoven-swap-hadron = { path = "crates/swap/hadron", optional = true }
beethoven-swap-raydium-cpmm = { path = "crates/swap/raydium-cpmm", optional = true }
beethoven-swap-unitas-vault = { path = "crates/swap/unitas-vault", optional = true }

[workspace]
members = [
Expand All @@ -100,6 +103,7 @@ members = [
"crates/swap/omnipair",
"crates/swap/hadron",
"crates/swap/raydium-cpmm",
"crates/swap/unitas-vault",
"program-test",
"crates/client",
]
Expand Down Expand Up @@ -127,6 +131,7 @@ beethoven-client = { path = "crates/client", features = [
"gamma",
"hadron",
"raydium_cpmm",
"unitas-vault",
"resolve",
] }
solana-rpc-client = "3"
Expand Down
2 changes: 2 additions & 0 deletions crates/client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ swap = [
"omnipair",
"hadron",
"raydium_cpmm",
"unitas-vault",
]

deposit = []
Expand All @@ -34,6 +35,7 @@ gamma = []
omnipair = []
hadron = []
raydium_cpmm = []
unitas-vault = []

[dependencies]
solana-address = { version = "2", features = ["curve25519"] }
Expand Down
9 changes: 9 additions & 0 deletions crates/client/src/swap/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ pub mod hadron;
#[cfg(feature = "raydium_cpmm")]
pub mod raydium_cpmm;

#[cfg(feature = "unitas-vault")]
pub mod unitas_vault;

use solana_address::Address;
#[cfg(feature = "resolve")]
use {
Expand Down Expand Up @@ -57,6 +60,9 @@ pub enum SwapProtocol {
},
#[cfg(feature = "raydium_cpmm")]
RaydiumCpmm { pool: Option<Address> },

#[cfg(feature = "unitas-vault")]
UnitasVault,
}

/// A single step in a multi-swap composition.
Expand Down Expand Up @@ -131,6 +137,9 @@ pub async fn resolve_swap(
SwapProtocol::RaydiumCpmm { pool } => {
raydium_cpmm::resolve(rpc, pool.as_ref(), mint_a, mint_b, user).await
}

#[cfg(feature = "unitas-vault")]
SwapProtocol::UnitasVault => unitas_vault::resolve(rpc, user).await,
}
}

Expand Down
79 changes: 79 additions & 0 deletions crates/client/src/swap/unitas_vault.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#[cfg(feature = "resolve")]
use {
crate::{get_associated_token_address, ClientError},
solana_rpc_client::nonblocking::rpc_client::RpcClient,
};
use {
crate::{ASSOCIATED_TOKEN_PROGRAM_ID, SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID},
solana_address::{address, Address},
solana_instruction::AccountMeta,
};

pub const UNITAS_VAULT_PROGRAM_ID: Address =
address!("VALT7AM76ZWfRhjVeYQRrLvNRLvqBzNs8dTsAcLW3jj");
pub const SUSDU_PROGRAM_ID: Address = address!("SUSD2TSk8DJodCkPviKb2okzbeQ597kCBjLVjq3G7pp");
pub const ACCESS_REGISTRY: Address = address!("8maav1g7bK1vRamXzADLUu3DQ7VmXxjVTJt9PbBuWcpd");
pub const VAULT_STAKE_POOL_USDU_TOKEN_ACCOUNT: Address =
address!("CFgrWjb9DYKVqf7QyQfmwjboDDkXpFHQ6292rnYxrjsa");
pub const SUSDU_MINTER: Address = address!("6ZY9KMGD9UjTX4tWGcw4Y4UHh14nzNmiEr92wTieYub5");
pub const USDU_MINT: Address = address!("9ckR7pPPvyPadACDTzLwK2ZAEeUJ3qGSnzPs8bVaHrSy");
pub const SUSDU_MINT: Address = address!("9iq5Q33RSiz1WcupHAQKbHBZkpn92UxBG2HfPWAZhMCa");
pub const VAULT_STATE: Address = address!("4x4h6NxSBsVJr5iQ4M7NfTqv8gUQrP4nE2psM6JQ2xn8");
pub const VAULT_CONFIG: Address = address!("DyiptL8AUJjxqphpkWAcVbFrA53EawpyaJ1VzDi8YoLc");
pub const SUSDU_CONFIG: Address = address!("6fMbMU14Q5sfiVHr8QY8wYbL7dDrV7N5vZrN8nQ3M2vN");

/// Pre-resolved addresses for building a Unitas vault stake_usdu_mint_susdu instruction offline.
pub struct UnitasVaultSwapInput {
pub caller: Address,
pub receiver: Address,
pub receiver_susdu_token_account: Address,
pub caller_usdu_token_account: Address,
}

/// Build Unitas vault stake_usdu_mint_susdu AccountMeta list from pre-resolved addresses (no RPC needed).
pub fn build_accounts(input: &UnitasVaultSwapInput) -> Vec<AccountMeta> {
vec![
AccountMeta::new_readonly(UNITAS_VAULT_PROGRAM_ID, false),
AccountMeta::new(input.caller, true),
AccountMeta::new(input.receiver, true),
AccountMeta::new(input.receiver_susdu_token_account, false),
AccountMeta::new(input.caller_usdu_token_account, false),
AccountMeta::new_readonly(ACCESS_REGISTRY, false),
AccountMeta::new(VAULT_STAKE_POOL_USDU_TOKEN_ACCOUNT, false),
AccountMeta::new_readonly(SUSDU_MINTER, false),
AccountMeta::new(USDU_MINT, false),
AccountMeta::new(SUSDU_MINT, false),
AccountMeta::new_readonly(VAULT_STATE, false),
AccountMeta::new(VAULT_CONFIG, false),
AccountMeta::new(SUSDU_CONFIG, false),
AccountMeta::new_readonly(SUSDU_PROGRAM_ID, false),
AccountMeta::new_readonly(TOKEN_2022_PROGRAM_ID, false),
AccountMeta::new_readonly(TOKEN_2022_PROGRAM_ID, false),
AccountMeta::new_readonly(ASSOCIATED_TOKEN_PROGRAM_ID, false),
AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
]
}

/// Resolve accounts and data for a Unitas vault stake_usdu_mint_susdu swap.
#[cfg(feature = "resolve")]
pub async fn resolve(
_rpc: &RpcClient,
user: &Address,
) -> Result<(Vec<AccountMeta>, Vec<u8>), ClientError> {
let input = UnitasVaultSwapInput {
caller: *user,
receiver: *user,
receiver_susdu_token_account: get_associated_token_address(
user,
&SUSDU_MINT,
&TOKEN_2022_PROGRAM_ID,
),
caller_usdu_token_account: get_associated_token_address(
user,
&USDU_MINT,
&TOKEN_2022_PROGRAM_ID,
),
};

Ok((build_accounts(&input), vec![]))
}
1 change: 1 addition & 0 deletions crates/client/tests/swap/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod gamma_discover;
mod manifest;
mod omnipair;
mod unitas_vault;
107 changes: 107 additions & 0 deletions crates/client/tests/swap/unitas_vault.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use {
beethoven_client::{
get_associated_token_address, resolve_swap,
swap::unitas_vault::{
ACCESS_REGISTRY, SUSDU_CONFIG, SUSDU_MINT, SUSDU_MINTER, SUSDU_PROGRAM_ID,
UNITAS_VAULT_PROGRAM_ID, USDU_MINT, VAULT_CONFIG, VAULT_STAKE_POOL_USDU_TOKEN_ACCOUNT,
VAULT_STATE,
},
SwapProtocol, ASSOCIATED_TOKEN_PROGRAM_ID, SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID,
},
solana_address::address,
solana_rpc_client::nonblocking::rpc_client::RpcClient,
};

fn get_rpc_url() -> String {
std::env::var("RPC_URL").unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string())
}

#[tokio::test]
async fn test_unitas_vault() {
let rpc = RpcClient::new(get_rpc_url());
let user = address!("11111111111111111111111111111112");

let (accounts, data) = resolve_swap(
&rpc,
&SwapProtocol::UnitasVault,
&USDU_MINT,
&SUSDU_MINT,
&user,
)
.await
.unwrap();

assert_eq!(accounts.len(), 18, "unitas_vault requires 18 accounts");

// Protocol program ID
assert_eq!(accounts[0].pubkey, UNITAS_VAULT_PROGRAM_ID);

// Caller account
assert_eq!(accounts[1].pubkey, user);
assert!(accounts[1].is_signer);
assert!(accounts[1].is_writable);

// Receiver account
assert_eq!(accounts[2].pubkey, user);
assert!(accounts[2].is_signer);
assert!(accounts[2].is_writable);

// Receiver sUSDU token account
let expected_receiver_susdu_token_account =
get_associated_token_address(&user, &SUSDU_MINT, &TOKEN_2022_PROGRAM_ID);
assert_eq!(accounts[3].pubkey, expected_receiver_susdu_token_account);
assert!(accounts[3].is_writable);

// Caller USDU token account
let expected_caller_usdu_token_account =
get_associated_token_address(&user, &USDU_MINT, &TOKEN_2022_PROGRAM_ID);
assert_eq!(accounts[4].pubkey, expected_caller_usdu_token_account);
assert!(accounts[4].is_writable);

// Access registry
assert_eq!(accounts[5].pubkey, ACCESS_REGISTRY);

// Vault stake pool USDU token account
assert_eq!(accounts[6].pubkey, VAULT_STAKE_POOL_USDU_TOKEN_ACCOUNT);
assert!(accounts[6].is_writable);

// sUSDU minter
assert_eq!(accounts[7].pubkey, SUSDU_MINTER);

// USDU token mint
assert_eq!(accounts[8].pubkey, USDU_MINT);
assert!(accounts[8].is_writable);

// sUSDU token mint
assert_eq!(accounts[9].pubkey, SUSDU_MINT);
assert!(accounts[9].is_writable);

// Vault state
assert_eq!(accounts[10].pubkey, VAULT_STATE);

// Vault config
assert_eq!(accounts[11].pubkey, VAULT_CONFIG);
assert!(accounts[11].is_writable);

// sUSDU config
assert_eq!(accounts[12].pubkey, SUSDU_CONFIG);
assert!(accounts[12].is_writable);

// sUSDU program
assert_eq!(accounts[13].pubkey, SUSDU_PROGRAM_ID);

// USDU token program
assert_eq!(accounts[14].pubkey, TOKEN_2022_PROGRAM_ID);

// sUSDU token program
assert_eq!(accounts[15].pubkey, TOKEN_2022_PROGRAM_ID);

// Associated token program
assert_eq!(accounts[16].pubkey, ASSOCIATED_TOKEN_PROGRAM_ID);

// System program
assert_eq!(accounts[17].pubkey, SYSTEM_PROGRAM_ID);

// No extra data
assert!(data.is_empty());
}
13 changes: 13 additions & 0 deletions crates/swap/unitas-vault/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "beethoven-swap-unitas-vault"
description = "Unitas vault 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"
Loading