├── .github ├── actions │ ├── setup-anchor-cli │ │ └── action.yml │ ├── setup-dep │ │ └── action.yml │ └── setup-solana │ │ └── action.yml └── workflows │ ├── ci_rust_test_on_pull.yml │ └── ci_ts_test_on_pull.yml ├── .gitignore ├── Anchor.toml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── README.md ├── programs └── vault │ ├── Cargo.toml │ ├── README.md │ ├── Xargo.toml │ └── src │ ├── context.rs │ ├── lib.rs │ ├── seed.rs │ ├── state.rs │ ├── strategy │ ├── apricot_without_lm.rs │ ├── base.rs │ ├── cypher.rs │ ├── frakt.rs │ ├── mango.rs │ ├── marginfi.rs │ └── mod.rs │ └── utils.rs ├── rust-client ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ ├── main.rs │ ├── user.rs │ └── utils.rs ├── rust-toolchain └── ts-client ├── .prettierrc ├── README.md ├── index.ts ├── jest.config.js ├── lib.es5.d.ts ├── package.json ├── pnpm-lock.yaml ├── src └── vault │ ├── affiliate-idl.ts │ ├── constants.ts │ ├── helper │ └── index.ts │ ├── idl.ts │ ├── index.ts │ ├── strategy │ ├── index.ts │ └── vault.ts │ ├── tests │ ├── affiliate.test.ts │ ├── utils │ │ └── index.ts │ └── vault.test.ts │ ├── types │ └── index.ts │ └── utils │ └── index.ts ├── tsconfig.build.json ├── tsconfig.esm.json └── tsconfig.json /.github/actions/setup-anchor-cli/action.yml: -------------------------------------------------------------------------------- 1 | name: "Setup anchor-cli" 2 | description: "Setup node js and anchor cli" 3 | runs: 4 | using: "composite" 5 | steps: 6 | - uses: actions/setup-node@v2 7 | - run: npm install -g @coral-xyz/anchor-cli@${{ env.ANCHOR_CLI_VERSION }} yarn 8 | shell: bash 9 | -------------------------------------------------------------------------------- /.github/actions/setup-dep/action.yml: -------------------------------------------------------------------------------- 1 | name: "Setup" 2 | description: "Setup program dependencies" 3 | runs: 4 | using: "composite" 5 | steps: 6 | - run: sudo apt-get update && sudo apt-get install -y ca-certificates pkg-config build-essential libudev-dev libdigest-sha-perl libssl-dev 7 | shell: bash 8 | -------------------------------------------------------------------------------- /.github/actions/setup-solana/action.yml: -------------------------------------------------------------------------------- 1 | name: "Setup Solana" 2 | description: "Setup Solana" 3 | runs: 4 | using: "composite" 5 | steps: 6 | - uses: actions/cache@v2 7 | name: Cache Solana Tool Suite 8 | id: cache-solana 9 | with: 10 | path: | 11 | ~/.cache/solana/ 12 | ~/.local/share/solana/ 13 | key: solana-${{ runner.os }}-v0000-${{ env.SOLANA_CLI_VERSION }} 14 | # - name: Check if Solana exists 15 | # id: check-solana-avail 16 | # run: solana --version 17 | # shell: bash 18 | - run: sh -c "$(curl -sSfL https://release.solana.com/v${{ env.SOLANA_CLI_VERSION }}/install)" 19 | shell: bash 20 | # if: steps.check-solana-avail.outputs.status == 'failure' 21 | - run: echo "$HOME/.local/share/solana/install/active_release/bin/" >> $GITHUB_PATH 22 | shell: bash 23 | - run: solana-keygen new --no-bip39-passphrase --force 24 | shell: bash 25 | # if: steps.check-solana-avail.outputs.status == 'failure' 26 | - run: solana config set --url localhost 27 | shell: bash 28 | # if: steps.check-solana-avail.outputs.status == 'failure' 29 | -------------------------------------------------------------------------------- /.github/workflows/ci_rust_test_on_pull.yml: -------------------------------------------------------------------------------- 1 | name: CI on pull_request 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - "docs/**" 7 | - "README.md" 8 | - "LICENSE" 9 | - ".editorconfig" 10 | branches: 11 | - main 12 | - develop 13 | - staging 14 | 15 | env: 16 | SOLANA_CLI_VERSION: 1.16.13 17 | ANCHOR_CLI_VERSION: 0.28.0 18 | 19 | jobs: 20 | changed_files_rust: 21 | runs-on: ubuntu-latest 22 | outputs: 23 | program: ${{steps.changed-files-specific.outputs.any_changed}} 24 | steps: 25 | - uses: actions/checkout@v2 26 | with: 27 | fetch-depth: 0 28 | - name: Get specific changed files 29 | id: changed-files-specific 30 | uses: tj-actions/changed-files@v18.6 31 | with: 32 | files: | 33 | rust-client 34 | programs 35 | 36 | rust-test: 37 | needs: changed_files_rust 38 | runs-on: ubuntu-latest 39 | if: needs.changed_files_rust.outputs.program == 'true' 40 | steps: 41 | - uses: actions/checkout@v2 42 | - uses: ./.github/actions/setup-dep 43 | - uses: ./.github/actions/setup-solana 44 | - uses: ./.github/actions/setup-anchor-cli 45 | # Install rust + toolchain 46 | - uses: actions-rs/toolchain@v1 47 | with: 48 | components: clippy, rustfmt 49 | # Cargo build cache 50 | - name: Cargo Cache 51 | uses: actions/cache@v1 52 | with: 53 | path: | 54 | ~/.cargo/ 55 | target 56 | key: ${{ runner.os }}-cargo-${{ steps.extract_branch.outputs.branch }} 57 | restore-keys: | 58 | ${{ runner.os }}-cargo-${{ steps.extract_branch.outputs.branch }} 59 | ${{ runner.os }}-cargo 60 | - name: Cargo fmt check 61 | run: cargo fmt -- --check 62 | shell: bash 63 | - name: Cargo test 64 | run: cargo test -- --nocapture 65 | shell: bash 66 | - name: Anchor build 67 | run: anchor build 68 | - name: Cargo build 69 | run: cargo build --package rust-client 70 | -------------------------------------------------------------------------------- /.github/workflows/ci_ts_test_on_pull.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration NPM 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - "docs/**" 7 | - "README.md" 8 | - "LICENSE" 9 | - ".editorconfig" 10 | branches: 11 | - main 12 | - develop 13 | - staging 14 | 15 | env: 16 | working_dir: ./ts-client 17 | 18 | jobs: 19 | changed_files_ts: 20 | runs-on: ubuntu-latest 21 | outputs: 22 | program: ${{steps.changed-files-specific.outputs.any_changed}} 23 | steps: 24 | - uses: actions/checkout@v2 25 | with: 26 | fetch-depth: 0 27 | - name: Get specific changed files 28 | id: changed-files-specific 29 | uses: tj-actions/changed-files@v18.6 30 | with: 31 | files: | 32 | ts-client 33 | 34 | ts-test: 35 | needs: changed_files_ts 36 | runs-on: ubuntu-latest 37 | if: needs.changed_files_ts.outputs.program == 'true' 38 | steps: 39 | - uses: actions/checkout@v2 40 | - name: Set up Node.js 41 | uses: actions/setup-node@v1 42 | with: 43 | node-version: ${{ matrix.node }} 44 | - uses: pnpm/action-setup@v4 45 | with: 46 | version: 6.0.2 47 | - name: Install dependencies 48 | working-directory: ${{ env.working_dir }} 49 | run: pnpm install 50 | - name: Run tests 51 | working-directory: ${{ env.working_dir }} 52 | run: pnpm run test 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .anchor 3 | .DS_Store 4 | target 5 | **/*.rs.bk 6 | node_modules 7 | 8 | ts-client/dist/ -------------------------------------------------------------------------------- /Anchor.toml: -------------------------------------------------------------------------------- 1 | [features] 2 | seeds = false 3 | [programs.localnet] 4 | mercurial_sdk = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" 5 | 6 | [registry] 7 | url = "https://anchor.projectserum.com" 8 | 9 | [provider] 10 | cluster = "localnet" 11 | wallet = "~/.config/solana/id.json" 12 | 13 | [scripts] 14 | test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | 12 | ### Changed 13 | 14 | ### Deprecated 15 | 16 | ### Removed 17 | 18 | ### Fixed 19 | 20 | ### Security 21 | 22 | ## @meteora-ag/vault-sdk[2.3.1] - PR [#135](https://github.com/mercurial-finance/vault-sdk/pull/135) 23 | 24 | ### Changed 25 | 26 | - remove `@solana/spl-token-registry` deps 27 | 28 | ## @meteora-ag/vault-sdk[2.3.0] - PR [#134](https://github.com/mercurial-finance/vault-sdk/pull/134) 29 | 30 | ### Changed 31 | 32 | - move to `meteora-ag` org 33 | 34 | ## @mercurial-finance/vault-sdk [2.2.1] - PR [#131](https://github.com/mercurial-finance/vault-sdk/pull/131) 35 | 36 | ### Added 37 | 38 | - new utils `deserializeMint` 39 | 40 | ## @mercurial-finance/vault-sdk [2.2.0] - PR [#130](https://github.com/mercurial-finance/vault-sdk/pull/130) 41 | 42 | ### Fixed 43 | 44 | - fix `tokenMint` & `tokenLpMint` 45 | 46 | ### Removed 47 | 48 | - remove `lpMintPda` field 49 | 50 | ## @mercurial-finance/vault-sdk [2.1.1] - PR [#129](https://github.com/mercurial-finance/vault-sdk/pull/129) 51 | 52 | ### Changed 53 | 54 | - update `createMultiple` & `createMultiplePda` to optimize rpc call 55 | 56 | ## @mercurial-finance/vault-sdk [2.1.0] - PR [#128](https://github.com/mercurial-finance/vault-sdk/pull/128) 57 | 58 | ### Fixed 59 | 60 | - fix `createMultiple` to take in less param 61 | 62 | ## @mercurial-finance/vault-sdk [2.0.1] - PR [#126](https://github.com/mercurial-finance/vault-sdk/pull/126) 63 | 64 | ### Fixed 65 | 66 | - fix `refreshVaultState` not working 67 | 68 | ## @mercurial-finance/vault-sdk [2.0.0] - PR [#124](https://github.com/mercurial-finance/vault-sdk/pull/124) 69 | 70 | ### Changed 71 | 72 | - Pump "@solana/spl-token" to 0.4.6 and various relevant packages 73 | 74 | ### Removed 75 | 76 | - Remove logic to withdraw directly from strategy 77 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "programs/*", 4 | "rust-client" 5 | ] 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mercurial Vault SDK 2 | 3 |

4 | 5 |

6 |
7 | 8 | ## Program ID: 9 | 10 | 24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi 11 | 12 | 13 | ## Getting started (Rust) 14 | Refer to [Rust Client Readme](https://github.com/mercurial-finance/vault-sdk/blob/main/rust-client/README.md) for full installation guide 15 | 16 | ### Affiliate 17 | Refer to [Affiliate Rust Client Readme](https://github.com/mercurial-finance/vault-periphery/tree/main/affiliate/rust-client) for affiliate Readme 18 | 19 |
20 | 21 | ## Getting started (TypeScript) 22 | NPM: https://www.npmjs.com/package/@meteora-ag/vault-sdk 23 | 24 | SDK: https://github.com/meteora-ag/vault-sdk 25 | 26 | Docs: https://docs.mercurial.finance/mercurial-dynamic-yield-infra/ 27 | 28 | Discord: https://discord.com/channels/841152225564950528/864859354335412224 29 | 30 |
31 | 32 | Refer to [Typescript Readme](https://github.com/meteora-ag/vault-sdk/blob/main/ts-client/README.md) for full installation guide 33 | 34 | ### Install & Run 35 | 36 | 1. `pnpm i` 37 | 2. `pnpm run test` 38 | 39 | ### Install & Run Affiliate 40 | 1. `pnpm i` 41 | 2. `pnpm run test-affiliate` -------------------------------------------------------------------------------- /programs/vault/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mercurial-vault" 3 | version = "0.5.0" 4 | description = "Created with Anchor" 5 | edition = "2018" 6 | license = "MIT" 7 | readme = "README.md" 8 | 9 | [lib] 10 | crate-type = ["cdylib", "lib"] 11 | name = "mercurial_vault" 12 | licence = "MIT" 13 | version = "0.0.1" 14 | 15 | [features] 16 | no-entrypoint = [] 17 | no-idl = [] 18 | cpi = ["no-entrypoint"] 19 | default = [] 20 | test-bpf = [] 21 | devnet = [] 22 | staging = [] 23 | no-capture = [] 24 | 25 | [dependencies] 26 | serde = { version = "1.0.136" } 27 | anchor-lang = "0.28.0" 28 | anchor-spl = "0.28.0" -------------------------------------------------------------------------------- /programs/vault/README.md: -------------------------------------------------------------------------------- 1 | ## VAULT SDK 2 | 3 | - Author: [Mercurial Finance](https://www.mercurial.finance/) 4 | - Purpose: SDK for Vault - Mercurial Finance 5 | 6 | - [Discord](https://discord.gg/yfGeKKbpM8) 7 | - [Github](hhttps://github.com/meteora-ag/vault-sdk) 8 | - [Crates](https://crates.io/crates/mercurial-vault) 9 | -------------------------------------------------------------------------------- /programs/vault/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /programs/vault/src/context.rs: -------------------------------------------------------------------------------- 1 | use crate::get_base_address_for_idle_vault; 2 | use crate::get_base_key; 3 | use crate::get_treasury_address; 4 | use crate::seed; 5 | use crate::state::{Strategy, Vault, MAX_BUMPS}; 6 | use anchor_lang::prelude::*; 7 | use anchor_spl::token::{Mint, Token, TokenAccount}; 8 | 9 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Debug)] 10 | pub struct VaultBumps { 11 | pub vault_bump: u8, 12 | pub token_vault_bump: u8, 13 | } 14 | 15 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Debug)] 16 | pub struct StrategyBumps { 17 | pub strategy_index: u8, 18 | pub strategy_bump: u8, 19 | pub collateral_vault_bump: u8, 20 | pub other_bumps: [u8; MAX_BUMPS], 21 | } 22 | 23 | #[derive(Accounts)] 24 | pub struct DepositWithdrawLiquidity<'info> { 25 | #[account( 26 | mut, 27 | has_one = token_vault, 28 | has_one = lp_mint, 29 | )] 30 | pub vault: Box>, 31 | #[account(mut)] 32 | pub token_vault: Account<'info, TokenAccount>, 33 | #[account(mut)] 34 | pub lp_mint: Account<'info, Mint>, 35 | #[account(mut)] 36 | pub user_token: Account<'info, TokenAccount>, 37 | #[account(mut)] 38 | pub user_lp: Account<'info, TokenAccount>, 39 | pub user: Signer<'info>, 40 | pub token_program: Program<'info, Token>, 41 | } 42 | 43 | #[derive(Accounts)] 44 | pub struct WithdrawDirectlyFromStrategy<'info> { 45 | #[account( 46 | mut, 47 | has_one = token_vault, 48 | has_one = lp_mint, 49 | has_one = fee_vault, 50 | )] 51 | pub vault: Box>, 52 | #[account(mut)] 53 | pub strategy: Box>, 54 | /// CHECK: Reserve account 55 | #[account(mut, constraint = strategy.reserve == reserve.key())] 56 | pub reserve: AccountInfo<'info>, 57 | /// CHECK: Strategy program 58 | pub strategy_program: AccountInfo<'info>, 59 | #[account( mut, constraint = strategy.collateral_vault == collateral_vault.key())] 60 | pub collateral_vault: Box>, 61 | #[account(mut)] 62 | pub token_vault: Account<'info, TokenAccount>, 63 | #[account(mut)] 64 | pub lp_mint: Account<'info, Mint>, 65 | #[account(mut)] 66 | pub fee_vault: Box>, 67 | #[account(mut)] 68 | pub user_token: Account<'info, TokenAccount>, 69 | #[account(mut)] 70 | pub user_lp: Account<'info, TokenAccount>, 71 | pub user: Signer<'info>, 72 | pub token_program: Program<'info, Token>, 73 | } 74 | 75 | #[derive(Accounts)] 76 | pub struct GetUnlockedAmount<'info> { 77 | pub vault: Box>, 78 | } 79 | 80 | #[derive(Accounts)] 81 | pub struct RebalanceStrategy<'info> { 82 | #[account( 83 | mut, 84 | has_one = token_vault, 85 | has_one = lp_mint, 86 | has_one = fee_vault, 87 | )] 88 | pub vault: Box>, 89 | #[account(mut)] 90 | pub strategy: Box>, 91 | 92 | #[account(mut)] 93 | pub token_vault: Box>, 94 | 95 | #[account(mut)] 96 | pub fee_vault: Box>, 97 | 98 | #[account(mut)] 99 | pub lp_mint: Box>, 100 | 101 | /// CHECK: Strategy program 102 | pub strategy_program: AccountInfo<'info>, 103 | 104 | #[account( mut, constraint = strategy.collateral_vault == collateral_vault.key())] 105 | pub collateral_vault: Box>, 106 | 107 | /// CHECK: Reserve account 108 | #[account(mut, constraint = strategy.reserve == reserve.key())] 109 | pub reserve: AccountInfo<'info>, 110 | 111 | pub token_program: Program<'info, Token>, 112 | 113 | #[account(constraint = vault.admin == operator.key() || vault.operator == operator.key())] 114 | pub operator: Signer<'info>, 115 | } 116 | /// Accounts for initialize a new vault 117 | #[derive(Accounts)] 118 | pub struct Initialize<'info> { 119 | /// This is base account for all vault 120 | /// No need base key now because we only allow 1 vault per token now 121 | // pub base: Signer<'info>, 122 | 123 | /// Vault account 124 | #[account( 125 | init, 126 | seeds = [ 127 | seed::VAULT_PREFIX.as_ref(), token_mint.key().as_ref(), get_base_key().as_ref() 128 | ], 129 | bump, 130 | payer = payer, 131 | space = 8 + std::mem::size_of::(), 132 | )] 133 | pub vault: Box>, 134 | 135 | /// Payer can be anyone 136 | #[account(mut)] 137 | pub payer: Signer<'info>, 138 | 139 | /// Token vault account 140 | #[account( 141 | init, 142 | seeds = [seed::TOKEN_VAULT_PREFIX.as_ref(), vault.key().as_ref()], 143 | bump, 144 | payer = payer, 145 | token::mint = token_mint, 146 | token::authority = vault, 147 | )] 148 | pub token_vault: Box>, 149 | /// Token mint account 150 | pub token_mint: Box>, // allocate some accounts in heap to avoid stack frame size limit 151 | #[account( 152 | init, 153 | seeds = [seed::LP_MINT_PREFIX.as_ref(), vault.key().as_ref()], 154 | bump, 155 | payer = payer, 156 | mint::decimals = token_mint.decimals, 157 | mint::authority = vault, 158 | )] 159 | pub lp_mint: Box>, 160 | /// rent 161 | pub rent: Sysvar<'info, Rent>, 162 | /// token_program 163 | pub token_program: Program<'info, Token>, 164 | /// system_program 165 | pub system_program: Program<'info, System>, 166 | } 167 | 168 | /// Accounts for InitializeIdleVault 169 | #[derive(Accounts)] 170 | pub struct InitializeIdleVault<'info> { 171 | /// Vault account 172 | #[account( 173 | init, 174 | seeds = [ 175 | seed::VAULT_PREFIX.as_ref(), token_mint.key().as_ref(), get_base_address_for_idle_vault().as_ref() 176 | ], 177 | bump, 178 | payer = payer, 179 | space = 8 + std::mem::size_of::(), 180 | )] 181 | pub vault: Box>, 182 | 183 | /// Payer can be anyone 184 | #[account(mut)] 185 | pub payer: Signer<'info>, 186 | 187 | /// Token vault account 188 | #[account( 189 | init, 190 | seeds = [seed::TOKEN_VAULT_PREFIX.as_ref(), vault.key().as_ref()], 191 | bump, 192 | payer = payer, 193 | token::mint = token_mint, 194 | token::authority = vault, 195 | )] 196 | pub token_vault: Box>, 197 | /// Token mint account 198 | pub token_mint: Box>, // allocate some accounts in heap to avoid stack frame size limit 199 | /// LP mint 200 | #[account( 201 | init, 202 | seeds = [seed::LP_MINT_PREFIX.as_ref(), vault.key().as_ref()], 203 | bump, 204 | payer = payer, 205 | mint::decimals = token_mint.decimals, 206 | mint::authority = vault, 207 | )] 208 | pub lp_mint: Box>, 209 | /// rent 210 | pub rent: Sysvar<'info, Rent>, 211 | /// token_program 212 | pub token_program: Program<'info, Token>, 213 | /// system_program 214 | pub system_program: Program<'info, System>, 215 | } 216 | -------------------------------------------------------------------------------- /programs/vault/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod context; 2 | pub mod seed; 3 | pub mod state; 4 | pub mod strategy; 5 | pub mod utils; 6 | 7 | use crate::strategy::base::StrategyType; 8 | use anchor_lang::prelude::*; 9 | use context::*; 10 | use std::convert::TryFrom; 11 | use std::str::FromStr; 12 | 13 | #[cfg(feature = "staging")] 14 | declare_id!("6YRZW57XsrT2DxSNLXHHQd4QmiqBode4d6btASkRqcFo"); 15 | 16 | #[cfg(not(feature = "staging"))] 17 | declare_id!("24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi"); 18 | 19 | // Performance fee when rebalancing 20 | pub const PERFORMANCE_FEE_NUMERATOR: u128 = 500u128; // 5% 21 | pub const PERFORMANCE_FEE_DENOMINATOR: u128 = 10000u128; 22 | 23 | // get vault address from base key and token mint 24 | // let (vault, _vault_bump) = Pubkey::find_program_address( 25 | // &[b"vault".as_ref(), token_mint.as_ref(), get_base_key().as_ref()], 26 | // &program_client.id(), 27 | // ); 28 | pub fn get_base_key() -> Pubkey { 29 | Pubkey::from_str("HWzXGcGHy4tcpYfaRDCyLNzXqBTv3E6BttpCH2vJxArv").unwrap() 30 | } 31 | 32 | /// Treasury address 33 | pub fn get_treasury_address() -> Pubkey { 34 | Pubkey::from_str("9kZeN47U2dubGbbzMrzzoRAUvpuxVLRcjW9XiFpYjUo4").unwrap() 35 | } 36 | 37 | pub fn get_base_address_for_idle_vault() -> Pubkey { 38 | Pubkey::default() 39 | } 40 | 41 | #[program] 42 | pub mod vault { 43 | use super::*; 44 | 45 | #[allow(unused_variables)] 46 | pub fn initialize(ctx: Context) -> Result<()> { 47 | Ok(()) 48 | } 49 | 50 | #[allow(unused_variables)] 51 | pub fn initialize_idle_vault(ctx: Context) -> Result<()> { 52 | Ok(()) 53 | } 54 | 55 | #[allow(unused_variables)] 56 | pub fn deposit( 57 | ctx: Context, 58 | token_amount: u64, 59 | minimum_lp_token_amount: u64, 60 | ) -> Result<()> { 61 | Ok(()) 62 | } 63 | 64 | #[allow(unused_variables)] 65 | pub fn withdraw( 66 | ctx: Context, 67 | unmint_amount: u64, 68 | min_out_amount: u64, 69 | ) -> Result<()> { 70 | Ok(()) 71 | } 72 | 73 | #[allow(unused_variables)] 74 | pub fn withdraw_directly_from_strategy<'a, 'b, 'c, 'info>( 75 | ctx: Context<'a, 'b, 'c, 'info, WithdrawDirectlyFromStrategy<'info>>, 76 | unmint_amount: u64, 77 | min_out_amount: u64, 78 | ) -> Result<()> { 79 | Ok(()) 80 | } 81 | 82 | // simulate function to get unlocked amount 83 | pub fn get_unlocked_amount(ctx: Context) -> Result<()> { 84 | let vault = &ctx.accounts.vault; 85 | let current_time = u64::try_from(Clock::get()?.unix_timestamp) 86 | .ok() 87 | .ok_or(VaultError::MathOverflow)?; 88 | let total_amount = vault 89 | .get_unlocked_amount(current_time) 90 | .ok_or(VaultError::MathOverflow)?; 91 | 92 | emit!(TotalAmount { total_amount }); 93 | 94 | Ok(()) 95 | } 96 | 97 | #[allow(unused_variables)] 98 | pub fn deposit_strategy<'a, 'b, 'c, 'info>( 99 | ctx: Context<'a, 'b, 'c, 'info, RebalanceStrategy<'info>>, 100 | amount: u64, 101 | ) -> Result<()> { 102 | Ok(()) 103 | } 104 | 105 | #[allow(unused_variables)] 106 | pub fn withdraw_strategy<'a, 'b, 'c, 'info>( 107 | ctx: Context<'a, 'b, 'c, 'info, RebalanceStrategy<'info>>, 108 | amount: u64, 109 | ) -> Result<()> { 110 | Ok(()) 111 | } 112 | } 113 | 114 | #[error_code] 115 | pub enum VaultError { 116 | #[msg("Vault is disabled")] 117 | VaultIsDisabled, 118 | 119 | #[msg("Exceeded slippage tolerance")] 120 | ExceededSlippage, 121 | 122 | #[msg("Strategy is not existed")] 123 | StrategyIsNotExisted, 124 | 125 | #[msg("UnAuthorized")] 126 | UnAuthorized, 127 | 128 | #[msg("Math operation overflow")] 129 | MathOverflow, 130 | 131 | #[msg("Protocol is not supported")] 132 | ProtocolIsNotSupported, 133 | 134 | #[msg("Reserve does not support token mint")] 135 | UnMatchReserve, 136 | 137 | #[msg("lockedProfitDegradation is invalid")] 138 | InvalidLockedProfitDegradation, 139 | 140 | #[msg("Maximum number of strategies have been reached")] 141 | MaxStrategyReached, 142 | 143 | #[msg("Strategy existed")] 144 | StrategyExisted, 145 | 146 | #[msg("Invalid unmint amount")] 147 | InvalidUnmintAmount, 148 | 149 | #[msg("Invalid accounts for strategy")] 150 | InvalidAccountsForStrategy, 151 | 152 | #[msg("Invalid bump")] 153 | InvalidBump, 154 | 155 | #[msg("Amount must be greater than 0")] 156 | AmountMustGreaterThanZero, 157 | 158 | #[msg("Mango is not supported anymore")] 159 | MangoIsNotSupportedAnymore, 160 | 161 | #[msg("Strategy is not supported")] 162 | StrategyIsNotSupported, 163 | 164 | #[msg("Pay amount is exceeded")] 165 | PayAmountIsExeeced, 166 | } 167 | 168 | #[event] 169 | pub struct AddLiquidity { 170 | pub lp_mint_amount: u64, 171 | pub token_amount: u64, 172 | } 173 | 174 | #[event] 175 | pub struct RemoveLiquidity { 176 | pub lpunmint_amount: u64, 177 | pub token_amount: u64, 178 | } 179 | #[event] 180 | pub struct StrategyDeposit { 181 | pub strategy_type: StrategyType, 182 | pub token_amount: u64, 183 | } 184 | 185 | #[event] 186 | pub struct StrategyWithdraw { 187 | pub strategy_type: StrategyType, 188 | pub collateral_amount: u64, 189 | pub estimated_token_amount: u64, 190 | } 191 | 192 | #[event] 193 | pub struct StakingReward { 194 | pub strategy_type: StrategyType, 195 | pub token_amount: u64, 196 | pub mint_account: Pubkey, 197 | } 198 | 199 | #[event] 200 | pub struct PerformanceFee { 201 | pub lp_mint_more: u64, 202 | } 203 | 204 | #[event] 205 | pub struct ReportLoss { 206 | pub strategy: Pubkey, 207 | pub loss: u64, 208 | } 209 | 210 | #[event] 211 | pub struct TotalAmount { 212 | pub total_amount: u64, 213 | } 214 | -------------------------------------------------------------------------------- /programs/vault/src/seed.rs: -------------------------------------------------------------------------------- 1 | pub static VAULT_PREFIX: &str = "vault"; 2 | pub static TOKEN_VAULT_PREFIX: &str = "token_vault"; 3 | pub static LP_MINT_PREFIX: &str = "lp_mint"; 4 | pub static COLLATERAL_VAULT_PREFIX: &str = "collateral_vault"; 5 | pub static FEE_VAULT_PREFIX: &str = "fee_vault"; 6 | pub static SOLEND_OBLIGATION_PREFIX: &str = "solend_obligation"; 7 | pub static SOLEND_OBLIGATION_OWNER_PREFIX: &str = "solend_obligation_owner"; 8 | pub static APRICOT_USER_INFO_SIGNER_PREFIX: &str = "apricot_user_info_signer"; 9 | -------------------------------------------------------------------------------- /programs/vault/src/state.rs: -------------------------------------------------------------------------------- 1 | use crate::context::VaultBumps; 2 | use crate::strategy::base::StrategyType; 3 | use anchor_lang::prelude::*; 4 | use std::convert::TryFrom; 5 | use std::fmt::Debug; 6 | 7 | pub const MAX_STRATEGY: usize = 30; 8 | pub const MAX_BUMPS: usize = 10; 9 | pub const LOCKED_PROFIT_DEGRADATION_DENOMINATOR: u128 = 1_000_000_000_000; 10 | 11 | #[account] 12 | #[derive(Default, Debug)] 13 | pub struct Vault { 14 | pub enabled: u8, 15 | pub bumps: VaultBumps, 16 | 17 | pub total_amount: u64, 18 | 19 | pub token_vault: Pubkey, 20 | pub fee_vault: Pubkey, 21 | pub token_mint: Pubkey, 22 | 23 | pub lp_mint: Pubkey, 24 | pub strategies: [Pubkey; MAX_STRATEGY], 25 | 26 | pub base: Pubkey, 27 | pub admin: Pubkey, 28 | pub operator: Pubkey, // person to send crank 29 | pub locked_profit_tracker: LockedProfitTracker, 30 | } 31 | 32 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Debug)] 33 | pub struct LockedProfitTracker { 34 | pub last_updated_locked_profit: u64, 35 | pub last_report: u64, 36 | pub locked_profit_degradation: u64, 37 | } 38 | 39 | impl Default for LockedProfitTracker { 40 | fn default() -> Self { 41 | LockedProfitTracker { 42 | last_updated_locked_profit: 0, 43 | last_report: 0, 44 | locked_profit_degradation: u64::try_from(LOCKED_PROFIT_DEGRADATION_DENOMINATOR) 45 | .unwrap() 46 | / (6 * 3600), // locked profit is fully dripped in 6 hour 47 | } 48 | } 49 | } 50 | impl LockedProfitTracker { 51 | // based from yearn vault 52 | // https://github.com/yearn/yearn-vaults/blob/main/contracts/Vault.vy#L825 53 | pub fn calculate_locked_profit(&self, current_time: u64) -> Option { 54 | let duration = u128::from(current_time.checked_sub(self.last_report)?); 55 | let locked_profit_degradation = u128::from(self.locked_profit_degradation); 56 | let locked_fund_ratio = duration.checked_mul(locked_profit_degradation)?; 57 | 58 | if locked_fund_ratio > LOCKED_PROFIT_DEGRADATION_DENOMINATOR { 59 | return Some(0); 60 | } 61 | let locked_profit = u128::from(self.last_updated_locked_profit); 62 | 63 | let locked_profit = (locked_profit 64 | .checked_mul(LOCKED_PROFIT_DEGRADATION_DENOMINATOR - locked_fund_ratio)?) 65 | .checked_div(LOCKED_PROFIT_DEGRADATION_DENOMINATOR)?; 66 | let locked_profit = u64::try_from(locked_profit).ok()?; 67 | Some(locked_profit) 68 | } 69 | } 70 | 71 | impl Vault { 72 | pub fn get_unlocked_amount(&self, current_time: u64) -> Option { 73 | self.total_amount.checked_sub( 74 | self.locked_profit_tracker 75 | .calculate_locked_profit(current_time)?, 76 | ) 77 | } 78 | 79 | pub fn get_amount_by_share( 80 | &self, 81 | current_time: u64, 82 | share: u64, 83 | total_supply: u64, 84 | ) -> Option { 85 | let total_amount = self.get_unlocked_amount(current_time)?; 86 | u64::try_from( 87 | u128::from(share) 88 | .checked_mul(u128::from(total_amount))? 89 | .checked_div(u128::from(total_supply))?, 90 | ) 91 | .ok() 92 | } 93 | 94 | pub fn get_unmint_amount( 95 | &self, 96 | current_time: u64, 97 | out_token: u64, 98 | total_supply: u64, 99 | ) -> Option { 100 | let total_amount = self.get_unlocked_amount(current_time)?; 101 | u64::try_from( 102 | u128::from(out_token) 103 | .checked_mul(u128::from(total_supply))? 104 | .checked_div(u128::from(total_amount))?, 105 | ) 106 | .ok() 107 | } 108 | 109 | pub fn is_strategy_existed(&self, pubkey: Pubkey) -> bool { 110 | for item in self.strategies.iter() { 111 | if *item == pubkey { 112 | return true; 113 | } 114 | } 115 | false 116 | } 117 | } 118 | 119 | impl Default for StrategyType { 120 | fn default() -> Self { 121 | StrategyType::PortFinanceWithoutLM 122 | } 123 | } 124 | 125 | #[account] 126 | #[derive(Default, Debug)] 127 | pub struct Strategy { 128 | pub reserve: Pubkey, 129 | pub collateral_vault: Pubkey, 130 | pub strategy_type: StrategyType, 131 | pub current_liquidity: u64, 132 | pub bumps: [u8; MAX_BUMPS], 133 | pub vault: Pubkey, 134 | pub is_disable: u8, 135 | } 136 | -------------------------------------------------------------------------------- /programs/vault/src/strategy/apricot_without_lm.rs: -------------------------------------------------------------------------------- 1 | use crate::ID; 2 | use anchor_lang::prelude::Pubkey; 3 | 4 | /// Return user signer PDA 5 | pub fn get_user_signer(strategy: &Pubkey) -> (Pubkey, u8) { 6 | Pubkey::find_program_address( 7 | &[ 8 | crate::seed::APRICOT_USER_INFO_SIGNER_PREFIX.as_ref(), 9 | strategy.as_ref(), 10 | ], 11 | &ID, 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /programs/vault/src/strategy/base.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use serde::{Deserialize, Serialize}; 3 | use std::str::FromStr; 4 | 5 | #[derive( 6 | AnchorSerialize, 7 | AnchorDeserialize, 8 | Clone, 9 | Copy, 10 | Debug, 11 | PartialEq, 12 | Serialize, 13 | Deserialize, 14 | Eq, 15 | Hash, 16 | )] 17 | pub enum StrategyType { 18 | PortFinanceWithoutLM, 19 | PortFinanceWithLM, 20 | SolendWithoutLM, 21 | Mango, // Mango is no longer supported 22 | SolendWithLM, 23 | ApricotWithoutLM, 24 | Francium, 25 | Tulip, 26 | // This is for compatibility with some administrative endpoint 27 | Vault, 28 | Drift, 29 | Frakt, 30 | Marginfi, 31 | Kamino, 32 | } 33 | 34 | impl std::fmt::Display for StrategyType { 35 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 36 | write!(f, "{:?}", self) 37 | // or, alternatively: 38 | // fmt::Debug::fmt(self, f) 39 | } 40 | } 41 | 42 | pub fn get_cypher_program_id() -> Pubkey { 43 | Pubkey::from_str("CYPH3o83JX6jY6NkbproSpdmQ5VWJtxjfJ5P8veyYVu3").unwrap() 44 | } 45 | 46 | pub fn get_tulip_program_id() -> Pubkey { 47 | Pubkey::from_str("4bcFeLv4nydFrsZqV5CgwCVrPhkQKsXtzfy2KyMz7ozM").unwrap() 48 | } 49 | 50 | pub fn get_francium_program_id() -> Pubkey { 51 | Pubkey::from_str("FC81tbGt6JWRXidaWYFXxGnTk4VgobhJHATvTRVMqgWj").unwrap() 52 | } 53 | 54 | pub fn get_apricot_program_id() -> Pubkey { 55 | Pubkey::from_str("6UeJYTLU1adaoHWeApWsoj1xNEDbWA2RhM2DLc8CrDDi").unwrap() 56 | } 57 | 58 | #[cfg(feature = "devnet")] 59 | pub fn get_port_finance_program_id() -> Pubkey { 60 | Pubkey::from_str("pdQ2rQQU5zH2rDgZ7xH2azMBJegUzUyunJ5Jd637hC4").unwrap() 61 | } 62 | 63 | #[cfg(not(feature = "devnet"))] 64 | pub fn get_port_finance_program_id() -> Pubkey { 65 | Pubkey::from_str("Port7uDYB3wk6GJAw4KT1WpTeMtSu9bTcChBHkX2LfR").unwrap() 66 | } 67 | 68 | #[cfg(feature = "devnet")] 69 | pub fn get_solend_program_id() -> Pubkey { 70 | Pubkey::from_str("ALend7Ketfx5bxh6ghsCDXAoDrhvEmsXT3cynB6aPLgx").unwrap() 71 | } 72 | 73 | #[cfg(not(feature = "devnet"))] 74 | pub fn get_solend_program_id() -> Pubkey { 75 | Pubkey::from_str("So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo").unwrap() 76 | } 77 | 78 | #[cfg(feature = "staging")] 79 | pub fn get_kamino_program_id() -> Pubkey { 80 | Pubkey::from_str("SLendK7ySfcEzyaFqy93gDnD3RtrpXJcnRwb6zFHJSh").unwrap() 81 | } 82 | 83 | #[cfg(not(feature = "staging"))] 84 | pub fn get_kamino_program_id() -> Pubkey { 85 | Pubkey::from_str("KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD").unwrap() 86 | } 87 | -------------------------------------------------------------------------------- /programs/vault/src/strategy/cypher.rs: -------------------------------------------------------------------------------- 1 | use crate::ID; 2 | use anchor_lang::prelude::Pubkey; 3 | 4 | /// Return strategy owner PDA 5 | pub fn get_strategy_owner(strategy_pubkey: &Pubkey) -> (Pubkey, u8) { 6 | Pubkey::find_program_address(&[b"cypher", strategy_pubkey.as_ref()], &ID) 7 | } 8 | -------------------------------------------------------------------------------- /programs/vault/src/strategy/frakt.rs: -------------------------------------------------------------------------------- 1 | use crate::ID; 2 | use anchor_lang::prelude::Pubkey; 3 | 4 | /// return vault owner 5 | pub fn get_strategy_owner(strategy_pubkey: &Pubkey) -> (Pubkey, u8) { 6 | Pubkey::find_program_address(&["frakt".as_ref(), strategy_pubkey.as_ref()], &ID) 7 | } 8 | -------------------------------------------------------------------------------- /programs/vault/src/strategy/mango.rs: -------------------------------------------------------------------------------- 1 | use crate::ID; 2 | use anchor_lang::prelude::Pubkey; 3 | 4 | /// Account num 5 | pub const ACCOUNT_NUM: u32 = 0u32; 6 | 7 | /// get strategy owner for mango 8 | pub fn get_strategy_owner(strategy_pubkey: &Pubkey) -> (Pubkey, u8) { 9 | Pubkey::find_program_address(&["mango".as_ref(), strategy_pubkey.as_ref()], &ID) 10 | } 11 | -------------------------------------------------------------------------------- /programs/vault/src/strategy/marginfi.rs: -------------------------------------------------------------------------------- 1 | use crate::ID; 2 | use anchor_lang::solana_program::pubkey::Pubkey; 3 | 4 | /// return strategy owner 5 | pub fn get_strategy_owner(strategy: &Pubkey) -> (Pubkey, u8) { 6 | Pubkey::find_program_address(&["marginfi_strategy".as_ref(), strategy.as_ref()], &ID) 7 | } 8 | 9 | /// get_marginfi_account 10 | pub fn get_marginfi_account(strategy: &Pubkey) -> (Pubkey, u8) { 11 | Pubkey::find_program_address(&["marginfi_account".as_ref(), strategy.as_ref()], &ID) 12 | } 13 | -------------------------------------------------------------------------------- /programs/vault/src/strategy/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod apricot_without_lm; 2 | pub mod base; 3 | pub mod cypher; 4 | pub mod frakt; 5 | pub mod mango; 6 | pub mod marginfi; 7 | -------------------------------------------------------------------------------- /programs/vault/src/utils.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::Pubkey; 2 | 3 | use crate::seed; 4 | 5 | pub fn derive_vault_address(token_mint: Pubkey, base: Pubkey) -> (Pubkey, u8) { 6 | Pubkey::find_program_address( 7 | &[ 8 | seed::VAULT_PREFIX.as_ref(), 9 | token_mint.as_ref(), 10 | base.as_ref(), 11 | ], 12 | &crate::ID, 13 | ) 14 | } 15 | 16 | pub fn derive_token_vault_address(vault: Pubkey) -> (Pubkey, u8) { 17 | Pubkey::find_program_address( 18 | &[seed::TOKEN_VAULT_PREFIX.as_ref(), vault.as_ref()], 19 | &crate::ID, 20 | ) 21 | } 22 | 23 | pub fn derive_strategy_address(vault: Pubkey, reserve: Pubkey, index: u8) -> (Pubkey, u8) { 24 | Pubkey::find_program_address(&[vault.as_ref(), reserve.as_ref(), &[index]], &crate::ID) 25 | } 26 | 27 | pub fn derive_collateral_vault_address(strategy: Pubkey) -> (Pubkey, u8) { 28 | Pubkey::find_program_address( 29 | &[seed::COLLATERAL_VAULT_PREFIX.as_ref(), strategy.as_ref()], 30 | &crate::ID, 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /rust-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-client" 3 | version = "0.5.0" 4 | edition = "2018" 5 | 6 | [features] 7 | devnet = [] 8 | 9 | [dependencies] 10 | anchor-lang = {version = "0.28.0"} 11 | anchor-spl = {version = "0.28.0"} 12 | anchor-client = {version = "0.28.0"} 13 | anyhow = "1.0" 14 | clap = {version = "3.0.10", features=["derive"]} 15 | mercurial-vault = { path = "../programs/vault", features = ["cpi"] } 16 | shellexpand = "2.1.0" 17 | spl-associated-token-account = { version = "2.1.0", features = ["no-entrypoint"] } 18 | rust_decimal="1.20.0" 19 | bincode = "^1.3.1" -------------------------------------------------------------------------------- /rust-client/README.md: -------------------------------------------------------------------------------- 1 | 2 | # CLI for mercurial vault 3 | 4 | ## Build 5 | 6 | `cargo build` 7 | 8 | ## Command 9 | 10 | Check command with `../target/debug/rust-client --help` 11 | 12 | ``` 13 | USAGE: 14 | rust-client [OPTIONS] 15 | 16 | OPTIONS: 17 | -h, --help Print help information 18 | --provider.admin 19 | --provider.base 20 | --provider.cluster Cluster override 21 | --provider.program_id Program id override 22 | --provider.token_mint Token mint override 23 | --provider.wallet Wallet override 24 | 25 | SUBCOMMANDS: 26 | deposit 27 | get-unlocked-amount 28 | help Print this message or the help of the given subcommand(s) 29 | show 30 | withdraw 31 | ``` 32 | 33 | 34 | ## Example 35 | 36 | ``` 37 | ../target/debug/rust-client show --provider.token_mint So11111111111111111111111111111111111111112 38 | 39 | ../target/debug/rust-client deposit 100 --provider.token_mint So11111111111111111111111111111111111111112 40 | 41 | ../target/debug/rust-client withdraw 100 --provider.token_mint So11111111111111111111111111111111111111112 42 | ``` -------------------------------------------------------------------------------- /rust-client/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod user; 2 | -------------------------------------------------------------------------------- /rust-client/src/main.rs: -------------------------------------------------------------------------------- 1 | mod user; 2 | mod utils; 3 | use anchor_client::solana_sdk::commitment_config::CommitmentConfig; 4 | use anchor_client::solana_sdk::pubkey::Pubkey; 5 | use anchor_client::solana_sdk::signature::Signer; 6 | use anchor_client::solana_sdk::signature::{read_keypair_file, Keypair}; 7 | use anchor_client::Client; 8 | use anchor_client::Cluster; 9 | use anchor_lang::solana_program::clock::Clock; 10 | use anchor_lang::solana_program::sysvar; 11 | use anyhow::Result; 12 | use bincode::deserialize; 13 | use clap::Parser; 14 | use mercurial_vault::get_base_key; 15 | use std::convert::TryFrom; 16 | use std::ops::Deref; 17 | use std::rc::Rc; 18 | use std::str::FromStr; 19 | use user::*; 20 | 21 | #[derive(Default, Debug, Parser)] 22 | pub struct ConfigOverride { 23 | /// Cluster override. 24 | #[clap(global = true, long = "provider.cluster")] 25 | pub cluster: Option, 26 | /// Wallet override. 27 | #[clap(global = true, long = "provider.wallet")] 28 | pub wallet: Option, 29 | 30 | /// Program id override 31 | #[clap(global = true, long = "provider.program_id")] 32 | pub program_id: Option, 33 | 34 | /// Token mint override 35 | #[clap(global = true, long = "provider.token_mint")] 36 | pub token_mint: Option, 37 | 38 | #[clap(global = true, long = "provider.base")] 39 | pub base: Option, 40 | } 41 | 42 | #[derive(Debug, Parser)] 43 | pub enum Command { 44 | Show {}, 45 | GetUnlockedAmount {}, 46 | #[clap(flatten)] 47 | User(UserCommand), 48 | } 49 | 50 | #[derive(Debug, Parser)] 51 | pub enum UserCommand { 52 | Deposit { token_amount: u64 }, 53 | Withdraw { unmint_amount: u64 }, 54 | } 55 | 56 | #[derive(Parser)] 57 | pub struct Opts { 58 | #[clap(flatten)] 59 | pub cfg_override: ConfigOverride, 60 | #[clap(subcommand)] 61 | pub command: Command, 62 | } 63 | fn main() -> Result<()> { 64 | let opts = Opts::parse(); 65 | 66 | let payer = match opts.cfg_override.wallet { 67 | Some(wallet) => read_keypair_file(wallet).expect("Requires a keypair file"), 68 | None => default_keypair(), 69 | }; 70 | let url = match opts.cfg_override.cluster { 71 | Some(cluster) => cluster, 72 | None => Cluster::Devnet, 73 | }; 74 | 75 | let client = Client::new_with_options( 76 | url, 77 | Rc::new(Keypair::from_bytes(&payer.to_bytes())?), 78 | CommitmentConfig::processed(), 79 | ); 80 | 81 | let program_id = match opts.cfg_override.program_id { 82 | Some(program_id) => Pubkey::from_str(&program_id).unwrap(), 83 | None => mercurial_vault::id(), 84 | }; 85 | 86 | let program_client = client.program(program_id)?; 87 | 88 | let token_mint = match opts.cfg_override.token_mint { 89 | Some(token_mint) => Pubkey::from_str(&token_mint).unwrap(), 90 | None => Pubkey::default(), 91 | }; 92 | 93 | let base = match opts.cfg_override.base { 94 | Some(base) => Pubkey::from_str(&base).unwrap(), 95 | None => get_base_key(), 96 | }; 97 | 98 | let (vault, _) = mercurial_vault::utils::derive_vault_address(token_mint, base); 99 | 100 | println!("ProgramID {}", program_id); 101 | println!("TOKEN MINT {}", token_mint); 102 | println!("Base {}", base); 103 | println!("VAULT {}", vault); 104 | 105 | // Fee payer is the admin 106 | match opts.command { 107 | Command::Show {} => show(&program_client, vault)?, 108 | Command::GetUnlockedAmount {} => get_unlocked_amount(&program_client, vault, &payer)?, 109 | Command::User(user) => match user { 110 | UserCommand::Deposit { token_amount } => { 111 | deposit(&program_client, token_mint, base, token_amount)? 112 | } 113 | UserCommand::Withdraw { unmint_amount } => { 114 | withdraw(&program_client, token_mint, base, unmint_amount)? 115 | } 116 | }, 117 | }; 118 | 119 | Ok(()) 120 | } 121 | 122 | fn show + Clone>( 123 | program_client: &anchor_client::Program, 124 | vault: Pubkey, 125 | ) -> Result<()> { 126 | let vault_data: mercurial_vault::state::Vault = program_client.account(vault)?; 127 | println!("VAULT DATA: {:#?}", vault_data); 128 | let token_mint: anchor_spl::token::Mint = program_client.account(vault_data.lp_mint)?; 129 | 130 | let current_timestamp = get_current_node_clock_time(program_client)?; 131 | 132 | println!( 133 | "TOTAL_AMOUNT: {}, TOTAL_UNLOCKED_AMOUNT: {}, lp_mint {}", 134 | vault_data.total_amount, 135 | vault_data.get_unlocked_amount(current_timestamp).unwrap(), 136 | token_mint.supply 137 | ); 138 | 139 | let token_data: anchor_spl::token::TokenAccount = 140 | program_client.account(vault_data.token_vault)?; 141 | 142 | println!("TOKEN AMOUNT: {}", token_data.amount); 143 | 144 | let mut strategy_amount = 0u64; 145 | for (_i, &strategy_pubkey) in vault_data.strategies.iter().enumerate() { 146 | if strategy_pubkey != Pubkey::default() { 147 | let strategy_state: mercurial_vault::state::Strategy = 148 | program_client.account(strategy_pubkey)?; 149 | 150 | println!("STRATEGY DATA {}: {:#?}", strategy_pubkey, strategy_state); 151 | 152 | strategy_amount += strategy_state.current_liquidity; 153 | } 154 | } 155 | assert_eq!(vault_data.total_amount, token_data.amount + strategy_amount); 156 | println!("Ok"); 157 | Ok(()) 158 | } 159 | 160 | pub fn get_current_node_clock_time + Clone>( 161 | program_client: &anchor_client::Program, 162 | ) -> Result { 163 | let rpc = program_client.rpc(); 164 | let clock_account = rpc.get_account(&sysvar::clock::id())?; 165 | let clock = deserialize::(&clock_account.data)?; 166 | let current_time = u64::try_from(clock.unix_timestamp)?; 167 | Ok(current_time) 168 | } 169 | 170 | fn get_unlocked_amount + Clone>( 171 | program_client: &anchor_client::Program, 172 | vault: Pubkey, 173 | payer: &Keypair, 174 | ) -> Result<()> { 175 | let builder = program_client 176 | .request() 177 | .accounts(mercurial_vault::accounts::GetUnlockedAmount { vault }) 178 | .args(mercurial_vault::instruction::GetUnlockedAmount {}); 179 | 180 | let simulation = utils::simulate_transaction(&builder, program_client, &vec![payer]).unwrap(); 181 | let logs = simulation.value.logs.expect("No log in simulation found"); 182 | let unlocked_amount: mercurial_vault::TotalAmount = 183 | utils::parse_event_log(&logs).expect("Event log not found"); 184 | println!("UNLOCKED AMOUNT: {}", unlocked_amount.total_amount); 185 | Ok(()) 186 | } 187 | 188 | pub fn default_keypair() -> Keypair { 189 | read_keypair_file(&*shellexpand::tilde("~/.config/solana/id.json")) 190 | .expect("Requires a keypair file") 191 | } 192 | -------------------------------------------------------------------------------- /rust-client/src/user.rs: -------------------------------------------------------------------------------- 1 | use anchor_client::solana_sdk::signature::Signer; 2 | use anchor_client::solana_sdk::signer::keypair::Keypair; 3 | use anchor_client::solana_sdk::system_instruction; 4 | use anchor_lang::solana_program::program_pack::Pack; 5 | use anchor_lang::solana_program::pubkey::Pubkey; 6 | use anchor_spl::token::spl_token; 7 | use anyhow::Result; 8 | use std::ops::Deref; 9 | 10 | pub fn deposit + Clone>( 11 | program_client: &anchor_client::Program, 12 | token_mint: Pubkey, 13 | base: Pubkey, 14 | token_amount: u64, 15 | ) -> Result<()> { 16 | println!("deposit {}", token_amount); 17 | 18 | let (vault, _vault_bump) = mercurial_vault::utils::derive_vault_address(token_mint, base); 19 | 20 | let (token_vault, _token_vault_bump) = 21 | mercurial_vault::utils::derive_token_vault_address(vault); 22 | 23 | let vault_state: mercurial_vault::state::Vault = program_client.account(vault)?; 24 | let lp_mint = vault_state.lp_mint; 25 | 26 | let user_token = get_or_create_ata(program_client, token_mint, program_client.payer())?; 27 | let user_lp = get_or_create_ata(program_client, lp_mint, program_client.payer())?; 28 | 29 | let builder = program_client 30 | .request() 31 | .accounts(mercurial_vault::accounts::DepositWithdrawLiquidity { 32 | vault, 33 | token_vault, 34 | lp_mint, 35 | user_token, 36 | user_lp, 37 | user: program_client.payer(), 38 | token_program: spl_token::id(), 39 | }) 40 | .args(mercurial_vault::instruction::Deposit { 41 | token_amount, 42 | minimum_lp_token_amount: 0, 43 | }); 44 | 45 | let signature = builder.send()?; 46 | println!("{}", signature); 47 | 48 | Ok(()) 49 | } 50 | 51 | pub fn withdraw + Clone>( 52 | program_client: &anchor_client::Program, 53 | token_mint: Pubkey, 54 | base: Pubkey, 55 | unmint_amount: u64, 56 | ) -> Result<()> { 57 | println!("withdraw {} lp token", unmint_amount); 58 | 59 | let (vault, _vault_bump) = mercurial_vault::utils::derive_vault_address(token_mint, base); 60 | 61 | let (token_vault, _token_vault_bump) = 62 | mercurial_vault::utils::derive_token_vault_address(vault); 63 | 64 | let vault_state: mercurial_vault::state::Vault = program_client.account(vault)?; 65 | let lp_mint = vault_state.lp_mint; 66 | 67 | let user_token = get_or_create_ata(program_client, token_mint, program_client.payer())?; 68 | let user_lp = get_or_create_ata(program_client, lp_mint, program_client.payer())?; 69 | 70 | let builder = program_client 71 | .request() 72 | .accounts(mercurial_vault::accounts::DepositWithdrawLiquidity { 73 | vault, 74 | token_vault, 75 | lp_mint, 76 | user_token, 77 | user_lp, 78 | user: program_client.payer(), 79 | token_program: spl_token::id(), 80 | }) 81 | .args(mercurial_vault::instruction::Withdraw { 82 | unmint_amount, 83 | min_out_amount: 0, 84 | }); 85 | 86 | let signature = builder.send()?; 87 | println!("{}", signature); 88 | 89 | Ok(()) 90 | } 91 | 92 | pub fn get_or_create_ata + Clone>( 93 | program_client: &anchor_client::Program, 94 | token_mint: Pubkey, 95 | user: Pubkey, 96 | ) -> Result { 97 | let user_token_account = 98 | spl_associated_token_account::get_associated_token_address(&user, &token_mint); 99 | let rpc_client = program_client.rpc(); 100 | if rpc_client.get_account_data(&user_token_account).is_err() { 101 | println!("Create ATA for TOKEN {} \n", &token_mint); 102 | 103 | let builder = program_client.request().instruction( 104 | spl_associated_token_account::create_associated_token_account( 105 | &program_client.payer(), 106 | &user, 107 | &token_mint, 108 | ), 109 | ); 110 | 111 | let signature = builder.send()?; 112 | println!("{}", signature); 113 | } 114 | Ok(user_token_account) 115 | } 116 | 117 | pub fn create_mint + Clone>( 118 | program_client: &anchor_client::Program, 119 | mint_keypair: &Keypair, 120 | authority: Pubkey, 121 | decimals: u8, 122 | ) -> Result<()> { 123 | let rpc = program_client.rpc(); 124 | 125 | let token_mint_account_rent = 126 | rpc.get_minimum_balance_for_rent_exemption(spl_token::state::Mint::LEN)?; 127 | 128 | let instructions = vec![ 129 | system_instruction::create_account( 130 | &program_client.payer(), 131 | &mint_keypair.pubkey(), 132 | token_mint_account_rent, 133 | spl_token::state::Mint::LEN as u64, 134 | &spl_token::id(), 135 | ), 136 | spl_token::instruction::initialize_mint( 137 | &spl_token::id(), 138 | &mint_keypair.pubkey(), 139 | &authority, 140 | None, 141 | decimals, 142 | ) 143 | .unwrap(), 144 | ]; 145 | 146 | let builder = program_client.request(); 147 | let builder = builder.signer(mint_keypair); 148 | let builder = instructions 149 | .into_iter() 150 | .fold(builder, |bld, ix| bld.instruction(ix)); 151 | let signature = builder.send()?; 152 | println!("{}", signature); 153 | Ok(()) 154 | } 155 | -------------------------------------------------------------------------------- /rust-client/src/utils.rs: -------------------------------------------------------------------------------- 1 | use anchor_client::solana_client::rpc_response::RpcSimulateTransactionResult; 2 | use anchor_client::RequestBuilder; 3 | use anchor_client::{ 4 | solana_client::rpc_response::Response, 5 | solana_sdk::{signature::Signer, transaction::Transaction}, 6 | Program, 7 | }; 8 | use core::ops::Deref; 9 | 10 | pub fn parse_event_log< 11 | T: anchor_lang::AnchorDeserialize + anchor_lang::AnchorSerialize + anchor_lang::Discriminator, 12 | >( 13 | logs: &Vec, 14 | ) -> Option { 15 | for log in logs.iter() { 16 | if log.starts_with("Program data:") { 17 | // Skip the prefix "Program data: " 18 | // Event logged has been changed to Program data: instead of Program log: 19 | // https://github.com/project-serum/anchor/pull/1608/files 20 | let log_info: String = log.chars().skip(14).collect(); 21 | let log_buf = anchor_lang::__private::base64::decode(log_info.as_bytes()); 22 | if log_buf.is_ok() { 23 | let log_buf = log_buf.unwrap(); 24 | // Check for event discriminator, it is a 8-byte prefix 25 | if log_buf[0..8] == T::discriminator() { 26 | // Skip event discriminator when deserialize 27 | return T::try_from_slice(&log_buf[8..]).ok(); 28 | } 29 | } 30 | } 31 | } 32 | None 33 | } 34 | 35 | pub fn simulate_transaction + Clone>( 36 | builder: &RequestBuilder, 37 | program: &Program, 38 | signers: &Vec<&dyn Signer>, 39 | ) -> Result, Box> { 40 | let instructions = builder.instructions()?; 41 | let rpc_client = program.rpc(); 42 | let recent_blockhash = rpc_client.get_latest_blockhash()?; 43 | let tx = Transaction::new_signed_with_payer( 44 | &instructions, 45 | Some(&program.payer()), 46 | signers, 47 | recent_blockhash, 48 | ); 49 | let simulation = rpc_client.simulate_transaction(&tx)?; 50 | Ok(simulation) 51 | } 52 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | 1.70.0 -------------------------------------------------------------------------------- /ts-client/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "tabWidth": 2 7 | } 8 | -------------------------------------------------------------------------------- /ts-client/README.md: -------------------------------------------------------------------------------- 1 | # Mercurial Vault SDK 2 | 3 |

4 | 5 |

6 |
7 | 8 | ## Getting started 9 | 10 | NPM: https://www.npmjs.com/package/@meteora-ag/vault-sdk 11 | 12 | SDK: https://github.com/meteora-ag/vault-sdk 13 | 14 | Docs: https://docs.mercurial.finance/mercurial-dynamic-yield-infra/ 15 | 16 | Discord: https://discord.com/channels/841152225564950528/864859354335412224 17 | 18 |
19 | 20 | ## Install 21 | 22 | 1. Install deps 23 | 24 | ``` 25 | npm i @meteora-ag/vault-sdk @coral-xyz/anchor @solana/web3.js@1 @solana/spl-token 26 | ``` 27 | 28 | 2. Initialize VaultImpl instance 29 | 30 | - Affiliate or partner? refer to the [Vault Affiliate Program]() 31 | 32 | ```ts 33 | import VaultImpl from '@meteora-ag/vault-sdk'; 34 | import { PublicKey } from '@solana/web3.js'; 35 | import { NATIVE_MINT } from "@solana/spl-token"; 36 | import { Wallet, AnchorProvider } from '@coral-xyz/anchor'; 37 | 38 | // Connection, Wallet, and AnchorProvider to interact with the network 39 | const mainnetConnection = new Connection('https://api.mainnet-beta.solana.com'); 40 | const mockWallet = new Wallet(new Keypair()); 41 | const provider = new AnchorProvider(mainnetConnection, mockWallet, { 42 | commitment: 'confirmed', 43 | }); 44 | 45 | const vaultImpl = await VaultImpl.create(connection, NATIVE_MINT); 46 | ``` 47 | 48 | 3. To interact with the VaultImpl 49 | 50 | ```ts 51 | // To refetch the vault's latest supply 52 | // Alternatively, use `vaultImpl.lpSupply` 53 | const lpSupply = await vaultImpl.getVaultSupply(); 54 | 55 | // Rewards are not instantly redeemable, and are subject to a lock. 56 | // This function returns the amount of LP that are redeemable. 57 | const unlockedAmount = await vaultImpl.getWithdrawableAmount(); 58 | 59 | // To deposit into the vault 60 | const amountInLamports = 1 * 10 ** 9; // 1.0 SOL 61 | const depositTx = await vaultImpl.deposit(mockWallet.publicKey, new BN(amountInLamports)); // Web3 Transaction Object 62 | const depositResult = await provider.sendAndConfirm(depositTx); // Transaction hash 63 | 64 | // Get the user's ATA LP balance 65 | const userLpBalance = await vaultImpl.getUserBalance(mockWallet.publicKey); 66 | 67 | // To withdraw from the vault 68 | const withdrawTx = await vaultImpl.withdraw(mockWallet.publicKey, new BN(userLpBalance)); // Web3 Transaction Object 69 | const withdrawResult = await provider.sendAndConfirm(withdrawTx); // Transaction hash 70 | ``` 71 | 72 | 4. Helper function 73 | 74 | ```ts 75 | import { helper } from '@meteora-ag/vault-sdk'; 76 | 77 | const userShare = await vaultImpl.getUserBalance(mockWallet.publicKey); 78 | const unlockedAmount = await vaultImpl.getWithdrawableAmount(); 79 | const lpSupply = await vaultImpl.getVaultSupply(); 80 | 81 | // To convert user's LP balance into underlying token amount 82 | const underlyingShare = helper.getAmountByShare(userShare, unlockedAmount, lpSupply); 83 | 84 | // To convert underlying token amount into user's LP balance 85 | const amountInLamports = 1 * 10 ** 9; // 1.0 SOL 86 | const lpToUnmint = helper.getUnmintAmount(new BN(amountInLamports), unlockedAmount, lpSupply); // To withdraw 1.0 SOL 87 | ``` 88 | 89 |
90 | 91 | ## Vault Affiliate 92 | 93 | To be a part of the Mercurial Finance's Vault Affiliate Program, visit our Discord above! 94 | 95 |
96 | 97 | #### To initialize vault with affiliate 98 | 99 | Affiliates only need to initialize the vault instance with the third paratemer `opt.affiliate`, subsequently, all interaction with the vault are the same as the usage guide above, no further configuration required. 100 | 101 | ```ts 102 | const vaultImpl = await VaultImpl.create( 103 | connection, 104 | NATIVE_MINT, 105 | { 106 | affiliateId: new PublicKey('YOUR_PARTNER_PUBLIC_KEY'); 107 | } 108 | ); 109 | ``` 110 | 111 | #### To check Partner info 112 | 113 | ```ts 114 | // Affiliate / Partner info 115 | const partnerInfo = await vaultImpl.getAffiliateInfo(); 116 | ``` 117 | -------------------------------------------------------------------------------- /ts-client/index.ts: -------------------------------------------------------------------------------- 1 | import VaultImpl from './src/vault/index'; 2 | import { PROGRAM_ID, AFFILIATE_PROGRAM_ID, KEEPER_URL } from './src/vault/constants'; 3 | import { getVaultPdas, getOnchainTime, getLpSupply } from './src/vault/utils'; 4 | import { getAmountByShare, getUnmintAmount, calculateWithdrawableAmount } from './src/vault/helper'; 5 | import { IDL } from './src/vault/idl'; 6 | 7 | export default VaultImpl; 8 | export { 9 | IDL, 10 | // Constant 11 | PROGRAM_ID, 12 | AFFILIATE_PROGRAM_ID, 13 | KEEPER_URL, 14 | // Utils 15 | getVaultPdas, 16 | getOnchainTime, 17 | getLpSupply, 18 | // Helper 19 | getAmountByShare, 20 | getUnmintAmount, 21 | calculateWithdrawableAmount, 22 | }; 23 | 24 | export type { VaultImplementation, VaultState, AffiliateInfo, ParsedClockState } from './src/vault/types'; 25 | export type { StrategyType } from './src/vault/strategy'; 26 | export type { Vault as VaultIdl } from './src/vault/idl'; 27 | -------------------------------------------------------------------------------- /ts-client/jest.config.js: -------------------------------------------------------------------------------- 1 | const TIMEOUT_SEC = 1000; 2 | 3 | module.exports = { 4 | preset: "ts-jest", 5 | testEnvironment: "node", 6 | transform: { 7 | "^.+\\.ts?$": "ts-jest", 8 | }, 9 | transformIgnorePatterns: ["/node_modules/"], 10 | testTimeout: TIMEOUT_SEC * 90, 11 | }; 12 | -------------------------------------------------------------------------------- /ts-client/lib.es5.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Fixes https://github.com/microsoft/TypeScript/issues/16655 for `Array.prototype.filter()` 3 | * For example, using the fix the type of `bar` is `string[]` in the below snippet as it should be. 4 | * 5 | * const foo: (string | null | undefined)[] = []; 6 | * const bar = foo.filter(Boolean); 7 | * 8 | * For related definitions, see https://github.com/microsoft/TypeScript/blob/master/src/lib/es5.d.ts 9 | * 10 | * Original licenses apply, see 11 | * - https://github.com/microsoft/TypeScript/blob/master/LICENSE.txt 12 | * - https://stackoverflow.com/help/licensing 13 | */ 14 | 15 | /** See https://stackoverflow.com/a/51390763/1470607 */ 16 | type Falsy = false | 0 | '' | null | undefined; 17 | 18 | interface Array { 19 | /** 20 | * Returns the elements of an array that meet the condition specified in a callback function. 21 | * @param predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array. 22 | * @param thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value. 23 | */ 24 | filter(predicate: BooleanConstructor, thisArg?: any): Exclude[]; 25 | } 26 | -------------------------------------------------------------------------------- /ts-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@meteora-ag/vault-sdk", 3 | "version": "2.3.1", 4 | "description": "Mercurial Vault SDK is a typescript library that allows you to interact with Meteora's dynamic vault.", 5 | "main": "dist/cjs/index.js", 6 | "module": "dist/esm/index.js", 7 | "scripts": { 8 | "build": "rm -rf dist && tsc -p tsconfig.build.json && tsc -p tsconfig.esm.json", 9 | "test": "jest ./src/vault/tests/*.test.ts --runInBand", 10 | "test-affiliate": "jest ./src/vault/tests/affiliate.test.ts --runInBand" 11 | }, 12 | "files": [ 13 | "dist" 14 | ], 15 | "keywords": [], 16 | "author": "meteora-ag", 17 | "license": "MIT", 18 | "dependencies": { 19 | "@coral-xyz/anchor": "^0.28.0", 20 | "@solana/buffer-layout": "^4.0.1", 21 | "@solana/spl-token": "^0.4.6", 22 | "@solana/web3.js": "^1.91.6", 23 | "bn.js": "5.2.1", 24 | "cross-fetch": "^3.1.5", 25 | "decimal.js": "10.3.1", 26 | "jsbi": "4.3.0" 27 | }, 28 | "devDependencies": { 29 | "@tsconfig/recommended": "^1.0.1", 30 | "@types/bn.js": "^5.1.0", 31 | "@types/chai": "^4.3.1", 32 | "@types/jest": "^27.5.1", 33 | "@types/mocha": "^9.1.1", 34 | "chai": "^4.3.6", 35 | "jest": "^28.1.0", 36 | "mocha": "^10.0.0", 37 | "ts-jest": "^28.0.2", 38 | "ts-mocha": "^10.0.0", 39 | "typescript": "^5.5.4" 40 | }, 41 | "directories": { 42 | "test": "tests" 43 | }, 44 | "repository": { 45 | "type": "git", 46 | "url": "git+https://github.com/MeteoraAg/vault-sdk.git" 47 | }, 48 | "bugs": { 49 | "url": "https://github.com/MeteoraAg/vault-sdk/issues" 50 | }, 51 | "homepage": "https://github.com/MeteoraAg/vault-sdk#readme" 52 | } 53 | -------------------------------------------------------------------------------- /ts-client/src/vault/affiliate-idl.ts: -------------------------------------------------------------------------------- 1 | export type AffiliateVault = { 2 | version: '0.1.0'; 3 | name: 'affiliate-vault'; 4 | instructions: [ 5 | { 6 | name: 'initPartner'; 7 | accounts: [ 8 | { 9 | name: 'partner'; 10 | isMut: true; 11 | isSigner: false; 12 | }, 13 | { 14 | name: 'vault'; 15 | isMut: false; 16 | isSigner: false; 17 | }, 18 | { 19 | name: 'partnerToken'; 20 | isMut: false; 21 | isSigner: false; 22 | }, 23 | { 24 | name: 'admin'; 25 | isMut: true; 26 | isSigner: true; 27 | }, 28 | { 29 | name: 'systemProgram'; 30 | isMut: false; 31 | isSigner: false; 32 | }, 33 | { 34 | name: 'rent'; 35 | isMut: false; 36 | isSigner: false; 37 | }, 38 | { 39 | name: 'tokenProgram'; 40 | isMut: false; 41 | isSigner: false; 42 | }, 43 | ]; 44 | args: []; 45 | }, 46 | { 47 | name: 'updateFeeRatio'; 48 | accounts: [ 49 | { 50 | name: 'partner'; 51 | isMut: true; 52 | isSigner: false; 53 | }, 54 | { 55 | name: 'admin'; 56 | isMut: false; 57 | isSigner: true; 58 | }, 59 | ]; 60 | args: [ 61 | { 62 | name: 'feeRatio'; 63 | type: 'u64'; 64 | }, 65 | ]; 66 | }, 67 | { 68 | name: 'initUser'; 69 | accounts: [ 70 | { 71 | name: 'user'; 72 | isMut: true; 73 | isSigner: false; 74 | }, 75 | { 76 | name: 'partner'; 77 | isMut: false; 78 | isSigner: false; 79 | }, 80 | { 81 | name: 'owner'; 82 | isMut: true; 83 | isSigner: true; 84 | }, 85 | { 86 | name: 'systemProgram'; 87 | isMut: false; 88 | isSigner: false; 89 | }, 90 | { 91 | name: 'rent'; 92 | isMut: false; 93 | isSigner: false; 94 | }, 95 | ]; 96 | args: []; 97 | }, 98 | { 99 | name: 'deposit'; 100 | accounts: [ 101 | { 102 | name: 'partner'; 103 | isMut: true; 104 | isSigner: false; 105 | }, 106 | { 107 | name: 'user'; 108 | isMut: true; 109 | isSigner: false; 110 | }, 111 | { 112 | name: 'vaultProgram'; 113 | isMut: false; 114 | isSigner: false; 115 | }, 116 | { 117 | name: 'vault'; 118 | isMut: true; 119 | isSigner: false; 120 | }, 121 | { 122 | name: 'tokenVault'; 123 | isMut: true; 124 | isSigner: false; 125 | }, 126 | { 127 | name: 'vaultLpMint'; 128 | isMut: true; 129 | isSigner: false; 130 | }, 131 | { 132 | name: 'userToken'; 133 | isMut: true; 134 | isSigner: false; 135 | }, 136 | { 137 | name: 'userLp'; 138 | isMut: true; 139 | isSigner: false; 140 | }, 141 | { 142 | name: 'owner'; 143 | isMut: false; 144 | isSigner: true; 145 | }, 146 | { 147 | name: 'tokenProgram'; 148 | isMut: false; 149 | isSigner: false; 150 | }, 151 | ]; 152 | args: [ 153 | { 154 | name: 'tokenAmount'; 155 | type: 'u64'; 156 | }, 157 | { 158 | name: 'minimumLpTokenAmount'; 159 | type: 'u64'; 160 | }, 161 | ]; 162 | }, 163 | { 164 | name: 'withdraw'; 165 | accounts: [ 166 | { 167 | name: 'partner'; 168 | isMut: true; 169 | isSigner: false; 170 | }, 171 | { 172 | name: 'user'; 173 | isMut: true; 174 | isSigner: false; 175 | }, 176 | { 177 | name: 'vaultProgram'; 178 | isMut: false; 179 | isSigner: false; 180 | }, 181 | { 182 | name: 'vault'; 183 | isMut: true; 184 | isSigner: false; 185 | }, 186 | { 187 | name: 'tokenVault'; 188 | isMut: true; 189 | isSigner: false; 190 | }, 191 | { 192 | name: 'vaultLpMint'; 193 | isMut: true; 194 | isSigner: false; 195 | }, 196 | { 197 | name: 'userToken'; 198 | isMut: true; 199 | isSigner: false; 200 | }, 201 | { 202 | name: 'userLp'; 203 | isMut: true; 204 | isSigner: false; 205 | }, 206 | { 207 | name: 'owner'; 208 | isMut: false; 209 | isSigner: true; 210 | }, 211 | { 212 | name: 'tokenProgram'; 213 | isMut: false; 214 | isSigner: false; 215 | }, 216 | ]; 217 | args: [ 218 | { 219 | name: 'unmintAmount'; 220 | type: 'u64'; 221 | }, 222 | { 223 | name: 'minOutAmount'; 224 | type: 'u64'; 225 | }, 226 | ]; 227 | }, 228 | { 229 | name: 'withdrawDirectlyFromStrategy'; 230 | accounts: [ 231 | { 232 | name: 'partner'; 233 | isMut: true; 234 | isSigner: false; 235 | }, 236 | { 237 | name: 'user'; 238 | isMut: true; 239 | isSigner: false; 240 | }, 241 | { 242 | name: 'vaultProgram'; 243 | isMut: false; 244 | isSigner: false; 245 | }, 246 | { 247 | name: 'vault'; 248 | isMut: true; 249 | isSigner: false; 250 | }, 251 | { 252 | name: 'strategy'; 253 | isMut: true; 254 | isSigner: false; 255 | }, 256 | { 257 | name: 'reserve'; 258 | isMut: true; 259 | isSigner: false; 260 | }, 261 | { 262 | name: 'strategyProgram'; 263 | isMut: false; 264 | isSigner: false; 265 | }, 266 | { 267 | name: 'collateralVault'; 268 | isMut: true; 269 | isSigner: false; 270 | }, 271 | { 272 | name: 'tokenVault'; 273 | isMut: true; 274 | isSigner: false; 275 | }, 276 | { 277 | name: 'vaultLpMint'; 278 | isMut: true; 279 | isSigner: false; 280 | }, 281 | { 282 | name: 'feeVault'; 283 | isMut: true; 284 | isSigner: false; 285 | }, 286 | { 287 | name: 'userToken'; 288 | isMut: true; 289 | isSigner: false; 290 | }, 291 | { 292 | name: 'userLp'; 293 | isMut: true; 294 | isSigner: false; 295 | }, 296 | { 297 | name: 'owner'; 298 | isMut: false; 299 | isSigner: true; 300 | }, 301 | { 302 | name: 'tokenProgram'; 303 | isMut: false; 304 | isSigner: false; 305 | }, 306 | ]; 307 | args: [ 308 | { 309 | name: 'unmintAmount'; 310 | type: 'u64'; 311 | }, 312 | { 313 | name: 'minOutAmount'; 314 | type: 'u64'; 315 | }, 316 | ]; 317 | }, 318 | { 319 | name: 'fundPartner'; 320 | accounts: [ 321 | { 322 | name: 'partner'; 323 | isMut: true; 324 | isSigner: false; 325 | }, 326 | { 327 | name: 'partnerToken'; 328 | isMut: true; 329 | isSigner: false; 330 | }, 331 | { 332 | name: 'funderToken'; 333 | isMut: true; 334 | isSigner: false; 335 | }, 336 | { 337 | name: 'funder'; 338 | isMut: false; 339 | isSigner: true; 340 | }, 341 | { 342 | name: 'tokenProgram'; 343 | isMut: false; 344 | isSigner: false; 345 | }, 346 | ]; 347 | args: [ 348 | { 349 | name: 'amount'; 350 | type: 'u64'; 351 | }, 352 | ]; 353 | }, 354 | ]; 355 | accounts: [ 356 | { 357 | name: 'partner'; 358 | type: { 359 | kind: 'struct'; 360 | fields: [ 361 | { 362 | name: 'partnerToken'; 363 | type: 'publicKey'; 364 | }, 365 | { 366 | name: 'vault'; 367 | type: 'publicKey'; 368 | }, 369 | { 370 | name: 'outstandingFee'; 371 | type: 'u64'; 372 | }, 373 | { 374 | name: 'feeRatio'; 375 | type: 'u64'; 376 | }, 377 | { 378 | name: 'cummulativeFee'; 379 | type: 'u128'; 380 | }, 381 | ]; 382 | }; 383 | }, 384 | { 385 | name: 'user'; 386 | type: { 387 | kind: 'struct'; 388 | fields: [ 389 | { 390 | name: 'owner'; 391 | type: 'publicKey'; 392 | }, 393 | { 394 | name: 'partner'; 395 | type: 'publicKey'; 396 | }, 397 | { 398 | name: 'currentVirtualPrice'; 399 | type: 'u64'; 400 | }, 401 | { 402 | name: 'lpToken'; 403 | type: 'u64'; 404 | }, 405 | { 406 | name: 'bump'; 407 | type: 'u8'; 408 | }, 409 | ]; 410 | }; 411 | }, 412 | ]; 413 | events: [ 414 | { 415 | name: 'ParnerFee'; 416 | fields: [ 417 | { 418 | name: 'fee'; 419 | type: 'u64'; 420 | index: false; 421 | }, 422 | ]; 423 | }, 424 | ]; 425 | errors: [ 426 | { 427 | code: 6000; 428 | name: 'MathOverflow'; 429 | msg: 'Math operation overflow'; 430 | }, 431 | { 432 | code: 6001; 433 | name: 'InvalidOwner'; 434 | msg: 'Invalid owner'; 435 | }, 436 | { 437 | code: 6002; 438 | name: 'InvalidFeeRatio'; 439 | msg: 'Invalid ratio'; 440 | }, 441 | { 442 | code: 6003; 443 | name: 'WrongFunderToken'; 444 | msg: 'Funder token account must be different from partner token account'; 445 | }, 446 | ]; 447 | }; 448 | 449 | export const IDL: AffiliateVault = { 450 | version: '0.1.0', 451 | name: 'affiliate-vault', 452 | instructions: [ 453 | { 454 | name: 'initPartner', 455 | accounts: [ 456 | { 457 | name: 'partner', 458 | isMut: true, 459 | isSigner: false, 460 | }, 461 | { 462 | name: 'vault', 463 | isMut: false, 464 | isSigner: false, 465 | }, 466 | { 467 | name: 'partnerToken', 468 | isMut: false, 469 | isSigner: false, 470 | }, 471 | { 472 | name: 'admin', 473 | isMut: true, 474 | isSigner: true, 475 | }, 476 | { 477 | name: 'systemProgram', 478 | isMut: false, 479 | isSigner: false, 480 | }, 481 | { 482 | name: 'rent', 483 | isMut: false, 484 | isSigner: false, 485 | }, 486 | { 487 | name: 'tokenProgram', 488 | isMut: false, 489 | isSigner: false, 490 | }, 491 | ], 492 | args: [], 493 | }, 494 | { 495 | name: 'updateFeeRatio', 496 | accounts: [ 497 | { 498 | name: 'partner', 499 | isMut: true, 500 | isSigner: false, 501 | }, 502 | { 503 | name: 'admin', 504 | isMut: false, 505 | isSigner: true, 506 | }, 507 | ], 508 | args: [ 509 | { 510 | name: 'feeRatio', 511 | type: 'u64', 512 | }, 513 | ], 514 | }, 515 | { 516 | name: 'initUser', 517 | accounts: [ 518 | { 519 | name: 'user', 520 | isMut: true, 521 | isSigner: false, 522 | }, 523 | { 524 | name: 'partner', 525 | isMut: false, 526 | isSigner: false, 527 | }, 528 | { 529 | name: 'owner', 530 | isMut: true, 531 | isSigner: true, 532 | }, 533 | { 534 | name: 'systemProgram', 535 | isMut: false, 536 | isSigner: false, 537 | }, 538 | { 539 | name: 'rent', 540 | isMut: false, 541 | isSigner: false, 542 | }, 543 | ], 544 | args: [], 545 | }, 546 | { 547 | name: 'deposit', 548 | accounts: [ 549 | { 550 | name: 'partner', 551 | isMut: true, 552 | isSigner: false, 553 | }, 554 | { 555 | name: 'user', 556 | isMut: true, 557 | isSigner: false, 558 | }, 559 | { 560 | name: 'vaultProgram', 561 | isMut: false, 562 | isSigner: false, 563 | }, 564 | { 565 | name: 'vault', 566 | isMut: true, 567 | isSigner: false, 568 | }, 569 | { 570 | name: 'tokenVault', 571 | isMut: true, 572 | isSigner: false, 573 | }, 574 | { 575 | name: 'vaultLpMint', 576 | isMut: true, 577 | isSigner: false, 578 | }, 579 | { 580 | name: 'userToken', 581 | isMut: true, 582 | isSigner: false, 583 | }, 584 | { 585 | name: 'userLp', 586 | isMut: true, 587 | isSigner: false, 588 | }, 589 | { 590 | name: 'owner', 591 | isMut: false, 592 | isSigner: true, 593 | }, 594 | { 595 | name: 'tokenProgram', 596 | isMut: false, 597 | isSigner: false, 598 | }, 599 | ], 600 | args: [ 601 | { 602 | name: 'tokenAmount', 603 | type: 'u64', 604 | }, 605 | { 606 | name: 'minimumLpTokenAmount', 607 | type: 'u64', 608 | }, 609 | ], 610 | }, 611 | { 612 | name: 'withdraw', 613 | accounts: [ 614 | { 615 | name: 'partner', 616 | isMut: true, 617 | isSigner: false, 618 | }, 619 | { 620 | name: 'user', 621 | isMut: true, 622 | isSigner: false, 623 | }, 624 | { 625 | name: 'vaultProgram', 626 | isMut: false, 627 | isSigner: false, 628 | }, 629 | { 630 | name: 'vault', 631 | isMut: true, 632 | isSigner: false, 633 | }, 634 | { 635 | name: 'tokenVault', 636 | isMut: true, 637 | isSigner: false, 638 | }, 639 | { 640 | name: 'vaultLpMint', 641 | isMut: true, 642 | isSigner: false, 643 | }, 644 | { 645 | name: 'userToken', 646 | isMut: true, 647 | isSigner: false, 648 | }, 649 | { 650 | name: 'userLp', 651 | isMut: true, 652 | isSigner: false, 653 | }, 654 | { 655 | name: 'owner', 656 | isMut: false, 657 | isSigner: true, 658 | }, 659 | { 660 | name: 'tokenProgram', 661 | isMut: false, 662 | isSigner: false, 663 | }, 664 | ], 665 | args: [ 666 | { 667 | name: 'unmintAmount', 668 | type: 'u64', 669 | }, 670 | { 671 | name: 'minOutAmount', 672 | type: 'u64', 673 | }, 674 | ], 675 | }, 676 | { 677 | name: 'withdrawDirectlyFromStrategy', 678 | accounts: [ 679 | { 680 | name: 'partner', 681 | isMut: true, 682 | isSigner: false, 683 | }, 684 | { 685 | name: 'user', 686 | isMut: true, 687 | isSigner: false, 688 | }, 689 | { 690 | name: 'vaultProgram', 691 | isMut: false, 692 | isSigner: false, 693 | }, 694 | { 695 | name: 'vault', 696 | isMut: true, 697 | isSigner: false, 698 | }, 699 | { 700 | name: 'strategy', 701 | isMut: true, 702 | isSigner: false, 703 | }, 704 | { 705 | name: 'reserve', 706 | isMut: true, 707 | isSigner: false, 708 | }, 709 | { 710 | name: 'strategyProgram', 711 | isMut: false, 712 | isSigner: false, 713 | }, 714 | { 715 | name: 'collateralVault', 716 | isMut: true, 717 | isSigner: false, 718 | }, 719 | { 720 | name: 'tokenVault', 721 | isMut: true, 722 | isSigner: false, 723 | }, 724 | { 725 | name: 'vaultLpMint', 726 | isMut: true, 727 | isSigner: false, 728 | }, 729 | { 730 | name: 'feeVault', 731 | isMut: true, 732 | isSigner: false, 733 | }, 734 | { 735 | name: 'userToken', 736 | isMut: true, 737 | isSigner: false, 738 | }, 739 | { 740 | name: 'userLp', 741 | isMut: true, 742 | isSigner: false, 743 | }, 744 | { 745 | name: 'owner', 746 | isMut: false, 747 | isSigner: true, 748 | }, 749 | { 750 | name: 'tokenProgram', 751 | isMut: false, 752 | isSigner: false, 753 | }, 754 | ], 755 | args: [ 756 | { 757 | name: 'unmintAmount', 758 | type: 'u64', 759 | }, 760 | { 761 | name: 'minOutAmount', 762 | type: 'u64', 763 | }, 764 | ], 765 | }, 766 | { 767 | name: 'fundPartner', 768 | accounts: [ 769 | { 770 | name: 'partner', 771 | isMut: true, 772 | isSigner: false, 773 | }, 774 | { 775 | name: 'partnerToken', 776 | isMut: true, 777 | isSigner: false, 778 | }, 779 | { 780 | name: 'funderToken', 781 | isMut: true, 782 | isSigner: false, 783 | }, 784 | { 785 | name: 'funder', 786 | isMut: false, 787 | isSigner: true, 788 | }, 789 | { 790 | name: 'tokenProgram', 791 | isMut: false, 792 | isSigner: false, 793 | }, 794 | ], 795 | args: [ 796 | { 797 | name: 'amount', 798 | type: 'u64', 799 | }, 800 | ], 801 | }, 802 | ], 803 | accounts: [ 804 | { 805 | name: 'partner', 806 | type: { 807 | kind: 'struct', 808 | fields: [ 809 | { 810 | name: 'partnerToken', 811 | type: 'publicKey', 812 | }, 813 | { 814 | name: 'vault', 815 | type: 'publicKey', 816 | }, 817 | { 818 | name: 'outstandingFee', 819 | type: 'u64', 820 | }, 821 | { 822 | name: 'feeRatio', 823 | type: 'u64', 824 | }, 825 | { 826 | name: 'cummulativeFee', 827 | type: 'u128', 828 | }, 829 | ], 830 | }, 831 | }, 832 | { 833 | name: 'user', 834 | type: { 835 | kind: 'struct', 836 | fields: [ 837 | { 838 | name: 'owner', 839 | type: 'publicKey', 840 | }, 841 | { 842 | name: 'partner', 843 | type: 'publicKey', 844 | }, 845 | { 846 | name: 'currentVirtualPrice', 847 | type: 'u64', 848 | }, 849 | { 850 | name: 'lpToken', 851 | type: 'u64', 852 | }, 853 | { 854 | name: 'bump', 855 | type: 'u8', 856 | }, 857 | ], 858 | }, 859 | }, 860 | ], 861 | events: [ 862 | { 863 | name: 'ParnerFee', 864 | fields: [ 865 | { 866 | name: 'fee', 867 | type: 'u64', 868 | index: false, 869 | }, 870 | ], 871 | }, 872 | ], 873 | errors: [ 874 | { 875 | code: 6000, 876 | name: 'MathOverflow', 877 | msg: 'Math operation overflow', 878 | }, 879 | { 880 | code: 6001, 881 | name: 'InvalidOwner', 882 | msg: 'Invalid owner', 883 | }, 884 | { 885 | code: 6002, 886 | name: 'InvalidFeeRatio', 887 | msg: 'Invalid ratio', 888 | }, 889 | { 890 | code: 6003, 891 | name: 'WrongFunderToken', 892 | msg: 'Funder token account must be different from partner token account', 893 | }, 894 | ], 895 | }; 896 | -------------------------------------------------------------------------------- /ts-client/src/vault/constants.ts: -------------------------------------------------------------------------------- 1 | import { Cluster, PublicKey } from '@solana/web3.js'; 2 | import { BN } from '@coral-xyz/anchor'; 3 | 4 | export const PROGRAM_ID = '24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi'; 5 | export const AFFILIATE_PROGRAM_ID = 'GacY9YuN16HNRTy7ZWwULPccwvfFSBeNLuAQP7y38Du3'; 6 | 7 | export const REWARDER = 'GuHrjvzqDvLTB27ebd9iFKwceCxKvSswzTByDQUTsvdm'; 8 | 9 | // Mainnet addresses 10 | export const USDC_MINT = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'); 11 | export const USDT_MINT = new PublicKey('Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB'); 12 | 13 | export const VAULT_BASE_KEY = new PublicKey('HWzXGcGHy4tcpYfaRDCyLNzXqBTv3E6BttpCH2vJxArv'); 14 | 15 | export const SEEDS = Object.freeze({ 16 | VAULT_PREFIX: 'vault', 17 | TOKEN_VAULT_PREFIX: 'token_vault', 18 | LP_MINT_PREFIX: 'lp_mint', 19 | COLLATERAL_VAULT_PREFIX: 'collateral_vault', 20 | OBLIGATION_PREFIX: 'obligation', 21 | OBLIGATION_OWNER_PREFIX: 'obligation_owner', 22 | STAKING_PREFIX: 'staking', 23 | MINER: 'Miner', 24 | QUARRY: 'Quarry', 25 | APRICOT_USER_INFO_SIGNER_PREFIX: 'apricot_user_info_signer', 26 | FRAKT: 'frakt', 27 | DEPOSIT: 'deposit', 28 | FRAKT_LENDING: 'nftlendingv2', 29 | CYPHER: 'cypher', 30 | MANGO_ACCOUNT: 'MangoAccount', 31 | MANGO: 'mango', 32 | PSYLEND: 'psylend', 33 | PSYLEND_OWNER: 'deposits', 34 | MARGINFI_STRATEGY: 'marginfi_strategy', 35 | MARGINFI_ACCOUNT: 'marginfi_account', 36 | }); 37 | 38 | export const StrategyProgram: Record< 39 | Cluster, 40 | { 41 | solend: PublicKey; 42 | portFinance: PublicKey; 43 | } 44 | > = { 45 | testnet: { 46 | solend: new PublicKey('ALend7Ketfx5bxh6ghsCDXAoDrhvEmsXT3cynB6aPLgx'), 47 | portFinance: new PublicKey('pdQ2rQQU5zH2rDgZ7xH2azMBJegUzUyunJ5Jd637hC4'), 48 | }, 49 | devnet: { 50 | solend: new PublicKey('ALend7Ketfx5bxh6ghsCDXAoDrhvEmsXT3cynB6aPLgx'), 51 | portFinance: new PublicKey('pdQ2rQQU5zH2rDgZ7xH2azMBJegUzUyunJ5Jd637hC4'), 52 | }, 53 | 'mainnet-beta': { 54 | solend: new PublicKey('So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo'), 55 | portFinance: new PublicKey('Port7uDYB3wk6GJAw4KT1WpTeMtSu9bTcChBHkX2LfR'), 56 | }, 57 | }; 58 | 59 | export const KEEPER_URL: Record = { 60 | testnet: 'https://staging-keeper.raccoons.dev', 61 | devnet: 'https://dev-keeper.raccoons.dev', 62 | 'mainnet-beta': 'https://merv2-api.mercurial.finance', 63 | }; 64 | 65 | export const VAULT_STRATEGY_ADDRESS = '11111111111111111111111111111111'; 66 | export const LOCKED_PROFIT_DEGRADATION_DENOMINATOR = new BN(1_000_000_000_000); 67 | -------------------------------------------------------------------------------- /ts-client/src/vault/helper/index.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js'; 2 | import { LOCKED_PROFIT_DEGRADATION_DENOMINATOR } from '../constants'; 3 | import { VaultState } from '../types'; 4 | 5 | /** 6 | * 7 | * @param share - user's share 8 | * @param withdrawableAmount - vault's withdrawable amount, vaultImpl.getWithdrawableAmount() 9 | * @param totalSupply - vault's total lp supply 10 | * @returns 11 | */ 12 | export function getAmountByShare(share: BN, withdrawableAmount: BN, totalSupply: BN): BN { 13 | return totalSupply.isZero() ? new BN(0) : share.mul(withdrawableAmount).div(totalSupply); 14 | } 15 | 16 | /** 17 | * 18 | * @param amount - amount of desired underlying token to unstake 19 | * @param withdrawableAmount - vault's withdrawable amount, vaultImpl.getWithdrawableAmount() 20 | * @param totalSupply - vault's total lp supply, vaultImpl.lpSupply 21 | * @returns BN 22 | */ 23 | export function getUnmintAmount(amount: BN, withdrawableAmount: BN, totalSupply: BN) { 24 | return amount.mul(totalSupply).div(withdrawableAmount); 25 | } 26 | 27 | /** 28 | * `calculateWithdrawableAmount` calculates the amount of funds that can be withdrawn from a vault with vault state provided 29 | * @param {number} onChainTime - the current time on the blockchain 30 | * @param {VaultState} vaultState - VaultState 31 | * @returns The amount of the vault that can be withdrawn. 32 | */ 33 | export function calculateWithdrawableAmount(onChainTime: number, vaultState: VaultState) { 34 | const { 35 | lockedProfitTracker: { lastReport, lockedProfitDegradation, lastUpdatedLockedProfit }, 36 | totalAmount: vaultTotalAmount, 37 | } = vaultState; 38 | 39 | const duration = new BN(onChainTime).sub(lastReport); 40 | 41 | const lockedFundRatio = duration.mul(lockedProfitDegradation); 42 | if (lockedFundRatio.gt(LOCKED_PROFIT_DEGRADATION_DENOMINATOR)) { 43 | return vaultTotalAmount; 44 | } 45 | 46 | const lockedProfit = lastUpdatedLockedProfit 47 | .mul(LOCKED_PROFIT_DEGRADATION_DENOMINATOR.sub(lockedFundRatio)) 48 | .div(LOCKED_PROFIT_DEGRADATION_DENOMINATOR); 49 | 50 | return vaultTotalAmount.sub(lockedProfit); 51 | } 52 | -------------------------------------------------------------------------------- /ts-client/src/vault/idl.ts: -------------------------------------------------------------------------------- 1 | export type Vault = { 2 | version: '0.7.2'; 3 | name: 'vault'; 4 | instructions: [ 5 | { 6 | name: 'initialize'; 7 | accounts: [ 8 | { 9 | name: 'vault'; 10 | isMut: true; 11 | isSigner: false; 12 | }, 13 | { 14 | name: 'payer'; 15 | isMut: true; 16 | isSigner: true; 17 | }, 18 | { 19 | name: 'tokenVault'; 20 | isMut: true; 21 | isSigner: false; 22 | }, 23 | { 24 | name: 'tokenMint'; 25 | isMut: false; 26 | isSigner: false; 27 | }, 28 | { 29 | name: 'lpMint'; 30 | isMut: true; 31 | isSigner: false; 32 | }, 33 | { 34 | name: 'rent'; 35 | isMut: false; 36 | isSigner: false; 37 | }, 38 | { 39 | name: 'tokenProgram'; 40 | isMut: false; 41 | isSigner: false; 42 | }, 43 | { 44 | name: 'systemProgram'; 45 | isMut: false; 46 | isSigner: false; 47 | }, 48 | ]; 49 | args: []; 50 | }, 51 | { 52 | name: 'enableVault'; 53 | accounts: [ 54 | { 55 | name: 'vault'; 56 | isMut: true; 57 | isSigner: false; 58 | }, 59 | { 60 | name: 'admin'; 61 | isMut: false; 62 | isSigner: true; 63 | }, 64 | ]; 65 | args: [ 66 | { 67 | name: 'enabled'; 68 | type: 'u8'; 69 | }, 70 | ]; 71 | }, 72 | { 73 | name: 'setOperator'; 74 | accounts: [ 75 | { 76 | name: 'vault'; 77 | isMut: true; 78 | isSigner: false; 79 | }, 80 | { 81 | name: 'operator'; 82 | isMut: false; 83 | isSigner: false; 84 | }, 85 | { 86 | name: 'admin'; 87 | isMut: false; 88 | isSigner: true; 89 | }, 90 | ]; 91 | args: []; 92 | }, 93 | { 94 | name: 'updateLockedProfitDegradation'; 95 | accounts: [ 96 | { 97 | name: 'vault'; 98 | isMut: true; 99 | isSigner: false; 100 | }, 101 | { 102 | name: 'admin'; 103 | isMut: false; 104 | isSigner: true; 105 | }, 106 | ]; 107 | args: [ 108 | { 109 | name: 'lockedProfitDegradation'; 110 | type: 'u64'; 111 | }, 112 | ]; 113 | }, 114 | { 115 | name: 'getUnlockedAmount'; 116 | accounts: [ 117 | { 118 | name: 'vault'; 119 | isMut: false; 120 | isSigner: false; 121 | }, 122 | ]; 123 | args: []; 124 | }, 125 | { 126 | name: 'transferAdmin'; 127 | accounts: [ 128 | { 129 | name: 'vault'; 130 | isMut: true; 131 | isSigner: false; 132 | }, 133 | { 134 | name: 'admin'; 135 | isMut: false; 136 | isSigner: true; 137 | }, 138 | { 139 | name: 'newAdmin'; 140 | isMut: false; 141 | isSigner: true; 142 | }, 143 | ]; 144 | args: []; 145 | }, 146 | { 147 | name: 'transferFeeVault'; 148 | accounts: [ 149 | { 150 | name: 'vault'; 151 | isMut: true; 152 | isSigner: false; 153 | }, 154 | { 155 | name: 'admin'; 156 | isMut: false; 157 | isSigner: true; 158 | }, 159 | { 160 | name: 'newFeeVault'; 161 | isMut: false; 162 | isSigner: false; 163 | }, 164 | ]; 165 | args: []; 166 | }, 167 | { 168 | name: 'initializeStrategy'; 169 | accounts: [ 170 | { 171 | name: 'vault'; 172 | isMut: true; 173 | isSigner: false; 174 | }, 175 | { 176 | name: 'strategyProgram'; 177 | isMut: false; 178 | isSigner: false; 179 | }, 180 | { 181 | name: 'strategy'; 182 | isMut: true; 183 | isSigner: false; 184 | }, 185 | { 186 | name: 'reserve'; 187 | isMut: true; 188 | isSigner: false; 189 | }, 190 | { 191 | name: 'collateralVault'; 192 | isMut: true; 193 | isSigner: false; 194 | }, 195 | { 196 | name: 'collateralMint'; 197 | isMut: false; 198 | isSigner: false; 199 | }, 200 | { 201 | name: 'admin'; 202 | isMut: true; 203 | isSigner: true; 204 | }, 205 | { 206 | name: 'systemProgram'; 207 | isMut: false; 208 | isSigner: false; 209 | }, 210 | { 211 | name: 'rent'; 212 | isMut: false; 213 | isSigner: false; 214 | }, 215 | { 216 | name: 'tokenProgram'; 217 | isMut: false; 218 | isSigner: false; 219 | }, 220 | ]; 221 | args: [ 222 | { 223 | name: 'bumps'; 224 | type: { 225 | defined: 'StrategyBumps'; 226 | }; 227 | }, 228 | { 229 | name: 'strategyType'; 230 | type: { 231 | defined: 'StrategyType'; 232 | }; 233 | }, 234 | ]; 235 | }, 236 | { 237 | name: 'removeStrategy'; 238 | accounts: [ 239 | { 240 | name: 'vault'; 241 | isMut: true; 242 | isSigner: false; 243 | }, 244 | { 245 | name: 'strategy'; 246 | isMut: true; 247 | isSigner: false; 248 | }, 249 | { 250 | name: 'strategyProgram'; 251 | isMut: false; 252 | isSigner: false; 253 | }, 254 | { 255 | name: 'collateralVault'; 256 | isMut: true; 257 | isSigner: false; 258 | }, 259 | { 260 | name: 'reserve'; 261 | isMut: true; 262 | isSigner: false; 263 | }, 264 | { 265 | name: 'tokenVault'; 266 | isMut: true; 267 | isSigner: false; 268 | }, 269 | { 270 | name: 'feeVault'; 271 | isMut: true; 272 | isSigner: false; 273 | }, 274 | { 275 | name: 'lpMint'; 276 | isMut: true; 277 | isSigner: false; 278 | }, 279 | { 280 | name: 'tokenProgram'; 281 | isMut: false; 282 | isSigner: false; 283 | }, 284 | { 285 | name: 'admin'; 286 | isMut: false; 287 | isSigner: true; 288 | }, 289 | ]; 290 | args: []; 291 | }, 292 | { 293 | name: 'removeStrategy2'; 294 | accounts: [ 295 | { 296 | name: 'vault'; 297 | isMut: true; 298 | isSigner: false; 299 | }, 300 | { 301 | name: 'strategy'; 302 | isMut: true; 303 | isSigner: false; 304 | }, 305 | { 306 | name: 'strategyProgram'; 307 | isMut: false; 308 | isSigner: false; 309 | }, 310 | { 311 | name: 'collateralVault'; 312 | isMut: true; 313 | isSigner: false; 314 | }, 315 | { 316 | name: 'reserve'; 317 | isMut: true; 318 | isSigner: false; 319 | }, 320 | { 321 | name: 'tokenVault'; 322 | isMut: true; 323 | isSigner: false; 324 | }, 325 | { 326 | name: 'tokenAdminAdvancePayment'; 327 | isMut: true; 328 | isSigner: false; 329 | }, 330 | { 331 | name: 'tokenVaultAdvancePayment'; 332 | isMut: true; 333 | isSigner: false; 334 | }, 335 | { 336 | name: 'feeVault'; 337 | isMut: true; 338 | isSigner: false; 339 | }, 340 | { 341 | name: 'lpMint'; 342 | isMut: true; 343 | isSigner: false; 344 | }, 345 | { 346 | name: 'tokenProgram'; 347 | isMut: false; 348 | isSigner: false; 349 | }, 350 | { 351 | name: 'admin'; 352 | isMut: false; 353 | isSigner: true; 354 | }, 355 | ]; 356 | args: [ 357 | { 358 | name: 'maxAdminPayAmount'; 359 | type: 'u64'; 360 | }, 361 | ]; 362 | }, 363 | { 364 | name: 'collectDust'; 365 | accounts: [ 366 | { 367 | name: 'vault'; 368 | isMut: false; 369 | isSigner: false; 370 | }, 371 | { 372 | name: 'tokenVault'; 373 | isMut: true; 374 | isSigner: false; 375 | }, 376 | { 377 | name: 'tokenAdmin'; 378 | isMut: true; 379 | isSigner: false; 380 | }, 381 | { 382 | name: 'admin'; 383 | isMut: false; 384 | isSigner: true; 385 | }, 386 | { 387 | name: 'tokenProgram'; 388 | isMut: false; 389 | isSigner: false; 390 | }, 391 | ]; 392 | args: []; 393 | }, 394 | { 395 | name: 'addStrategy'; 396 | accounts: [ 397 | { 398 | name: 'vault'; 399 | isMut: true; 400 | isSigner: false; 401 | }, 402 | { 403 | name: 'strategy'; 404 | isMut: false; 405 | isSigner: false; 406 | }, 407 | { 408 | name: 'admin'; 409 | isMut: false; 410 | isSigner: true; 411 | }, 412 | ]; 413 | args: []; 414 | }, 415 | { 416 | name: 'depositStrategy'; 417 | accounts: [ 418 | { 419 | name: 'vault'; 420 | isMut: true; 421 | isSigner: false; 422 | }, 423 | { 424 | name: 'strategy'; 425 | isMut: true; 426 | isSigner: false; 427 | }, 428 | { 429 | name: 'tokenVault'; 430 | isMut: true; 431 | isSigner: false; 432 | }, 433 | { 434 | name: 'feeVault'; 435 | isMut: true; 436 | isSigner: false; 437 | }, 438 | { 439 | name: 'lpMint'; 440 | isMut: true; 441 | isSigner: false; 442 | }, 443 | { 444 | name: 'strategyProgram'; 445 | isMut: false; 446 | isSigner: false; 447 | }, 448 | { 449 | name: 'collateralVault'; 450 | isMut: true; 451 | isSigner: false; 452 | }, 453 | { 454 | name: 'reserve'; 455 | isMut: true; 456 | isSigner: false; 457 | }, 458 | { 459 | name: 'tokenProgram'; 460 | isMut: false; 461 | isSigner: false; 462 | }, 463 | { 464 | name: 'operator'; 465 | isMut: false; 466 | isSigner: true; 467 | }, 468 | ]; 469 | args: [ 470 | { 471 | name: 'amount'; 472 | type: 'u64'; 473 | }, 474 | ]; 475 | }, 476 | { 477 | name: 'withdrawStrategy'; 478 | accounts: [ 479 | { 480 | name: 'vault'; 481 | isMut: true; 482 | isSigner: false; 483 | }, 484 | { 485 | name: 'strategy'; 486 | isMut: true; 487 | isSigner: false; 488 | }, 489 | { 490 | name: 'tokenVault'; 491 | isMut: true; 492 | isSigner: false; 493 | }, 494 | { 495 | name: 'feeVault'; 496 | isMut: true; 497 | isSigner: false; 498 | }, 499 | { 500 | name: 'lpMint'; 501 | isMut: true; 502 | isSigner: false; 503 | }, 504 | { 505 | name: 'strategyProgram'; 506 | isMut: false; 507 | isSigner: false; 508 | }, 509 | { 510 | name: 'collateralVault'; 511 | isMut: true; 512 | isSigner: false; 513 | }, 514 | { 515 | name: 'reserve'; 516 | isMut: true; 517 | isSigner: false; 518 | }, 519 | { 520 | name: 'tokenProgram'; 521 | isMut: false; 522 | isSigner: false; 523 | }, 524 | { 525 | name: 'operator'; 526 | isMut: false; 527 | isSigner: true; 528 | }, 529 | ]; 530 | args: [ 531 | { 532 | name: 'amount'; 533 | type: 'u64'; 534 | }, 535 | ]; 536 | }, 537 | { 538 | name: 'claimRewards'; 539 | accounts: [ 540 | { 541 | name: 'vault'; 542 | isMut: false; 543 | isSigner: false; 544 | }, 545 | { 546 | name: 'strategy'; 547 | isMut: false; 548 | isSigner: false; 549 | }, 550 | { 551 | name: 'tokenProgram'; 552 | isMut: false; 553 | isSigner: false; 554 | }, 555 | { 556 | name: 'tokenRewardAcc'; 557 | isMut: true; 558 | isSigner: false; 559 | }, 560 | { 561 | name: 'operator'; 562 | isMut: false; 563 | isSigner: true; 564 | }, 565 | ]; 566 | args: []; 567 | }, 568 | { 569 | name: 'withdraw2'; 570 | accounts: [ 571 | { 572 | name: 'vault'; 573 | isMut: true; 574 | isSigner: false; 575 | }, 576 | { 577 | name: 'tokenVault'; 578 | isMut: true; 579 | isSigner: false; 580 | }, 581 | { 582 | name: 'lpMint'; 583 | isMut: true; 584 | isSigner: false; 585 | }, 586 | { 587 | name: 'userToken'; 588 | isMut: true; 589 | isSigner: false; 590 | }, 591 | { 592 | name: 'userLp'; 593 | isMut: true; 594 | isSigner: false; 595 | }, 596 | { 597 | name: 'user'; 598 | isMut: false; 599 | isSigner: true; 600 | }, 601 | { 602 | name: 'tokenProgram'; 603 | isMut: false; 604 | isSigner: false; 605 | }, 606 | ]; 607 | args: [ 608 | { 609 | name: 'unmintAmount'; 610 | type: 'u64'; 611 | }, 612 | { 613 | name: 'minOutAmount'; 614 | type: 'u64'; 615 | }, 616 | ]; 617 | }, 618 | { 619 | name: 'deposit'; 620 | accounts: [ 621 | { 622 | name: 'vault'; 623 | isMut: true; 624 | isSigner: false; 625 | }, 626 | { 627 | name: 'tokenVault'; 628 | isMut: true; 629 | isSigner: false; 630 | }, 631 | { 632 | name: 'lpMint'; 633 | isMut: true; 634 | isSigner: false; 635 | }, 636 | { 637 | name: 'userToken'; 638 | isMut: true; 639 | isSigner: false; 640 | }, 641 | { 642 | name: 'userLp'; 643 | isMut: true; 644 | isSigner: false; 645 | }, 646 | { 647 | name: 'user'; 648 | isMut: false; 649 | isSigner: true; 650 | }, 651 | { 652 | name: 'tokenProgram'; 653 | isMut: false; 654 | isSigner: false; 655 | }, 656 | ]; 657 | args: [ 658 | { 659 | name: 'tokenAmount'; 660 | type: 'u64'; 661 | }, 662 | { 663 | name: 'minimumLpTokenAmount'; 664 | type: 'u64'; 665 | }, 666 | ]; 667 | }, 668 | { 669 | name: 'withdraw'; 670 | accounts: [ 671 | { 672 | name: 'vault'; 673 | isMut: true; 674 | isSigner: false; 675 | }, 676 | { 677 | name: 'tokenVault'; 678 | isMut: true; 679 | isSigner: false; 680 | }, 681 | { 682 | name: 'lpMint'; 683 | isMut: true; 684 | isSigner: false; 685 | }, 686 | { 687 | name: 'userToken'; 688 | isMut: true; 689 | isSigner: false; 690 | }, 691 | { 692 | name: 'userLp'; 693 | isMut: true; 694 | isSigner: false; 695 | }, 696 | { 697 | name: 'user'; 698 | isMut: false; 699 | isSigner: true; 700 | }, 701 | { 702 | name: 'tokenProgram'; 703 | isMut: false; 704 | isSigner: false; 705 | }, 706 | ]; 707 | args: [ 708 | { 709 | name: 'unmintAmount'; 710 | type: 'u64'; 711 | }, 712 | { 713 | name: 'minOutAmount'; 714 | type: 'u64'; 715 | }, 716 | ]; 717 | }, 718 | { 719 | name: 'withdrawDirectlyFromStrategy'; 720 | accounts: [ 721 | { 722 | name: 'vault'; 723 | isMut: true; 724 | isSigner: false; 725 | }, 726 | { 727 | name: 'strategy'; 728 | isMut: true; 729 | isSigner: false; 730 | }, 731 | { 732 | name: 'reserve'; 733 | isMut: true; 734 | isSigner: false; 735 | }, 736 | { 737 | name: 'strategyProgram'; 738 | isMut: false; 739 | isSigner: false; 740 | }, 741 | { 742 | name: 'collateralVault'; 743 | isMut: true; 744 | isSigner: false; 745 | }, 746 | { 747 | name: 'tokenVault'; 748 | isMut: true; 749 | isSigner: false; 750 | }, 751 | { 752 | name: 'lpMint'; 753 | isMut: true; 754 | isSigner: false; 755 | }, 756 | { 757 | name: 'feeVault'; 758 | isMut: true; 759 | isSigner: false; 760 | }, 761 | { 762 | name: 'userToken'; 763 | isMut: true; 764 | isSigner: false; 765 | }, 766 | { 767 | name: 'userLp'; 768 | isMut: true; 769 | isSigner: false; 770 | }, 771 | { 772 | name: 'user'; 773 | isMut: false; 774 | isSigner: true; 775 | }, 776 | { 777 | name: 'tokenProgram'; 778 | isMut: false; 779 | isSigner: false; 780 | }, 781 | ]; 782 | args: [ 783 | { 784 | name: 'unmintAmount'; 785 | type: 'u64'; 786 | }, 787 | { 788 | name: 'minOutAmount'; 789 | type: 'u64'; 790 | }, 791 | ]; 792 | }, 793 | ]; 794 | accounts: [ 795 | { 796 | name: 'vault'; 797 | type: { 798 | kind: 'struct'; 799 | fields: [ 800 | { 801 | name: 'enabled'; 802 | type: 'u8'; 803 | }, 804 | { 805 | name: 'bumps'; 806 | type: { 807 | defined: 'VaultBumps'; 808 | }; 809 | }, 810 | { 811 | name: 'totalAmount'; 812 | type: 'u64'; 813 | }, 814 | { 815 | name: 'tokenVault'; 816 | type: 'publicKey'; 817 | }, 818 | { 819 | name: 'feeVault'; 820 | type: 'publicKey'; 821 | }, 822 | { 823 | name: 'tokenMint'; 824 | type: 'publicKey'; 825 | }, 826 | { 827 | name: 'lpMint'; 828 | type: 'publicKey'; 829 | }, 830 | { 831 | name: 'strategies'; 832 | type: { 833 | array: ['publicKey', 30]; 834 | }; 835 | }, 836 | { 837 | name: 'base'; 838 | type: 'publicKey'; 839 | }, 840 | { 841 | name: 'admin'; 842 | type: 'publicKey'; 843 | }, 844 | { 845 | name: 'operator'; 846 | type: 'publicKey'; 847 | }, 848 | { 849 | name: 'lockedProfitTracker'; 850 | type: { 851 | defined: 'LockedProfitTracker'; 852 | }; 853 | }, 854 | ]; 855 | }; 856 | }, 857 | { 858 | name: 'strategy'; 859 | type: { 860 | kind: 'struct'; 861 | fields: [ 862 | { 863 | name: 'reserve'; 864 | type: 'publicKey'; 865 | }, 866 | { 867 | name: 'collateralVault'; 868 | type: 'publicKey'; 869 | }, 870 | { 871 | name: 'strategyType'; 872 | type: { 873 | defined: 'StrategyType'; 874 | }; 875 | }, 876 | { 877 | name: 'currentLiquidity'; 878 | type: 'u64'; 879 | }, 880 | { 881 | name: 'bumps'; 882 | type: { 883 | array: ['u8', 10]; 884 | }; 885 | }, 886 | { 887 | name: 'vault'; 888 | type: 'publicKey'; 889 | }, 890 | { 891 | name: 'isDisable'; 892 | type: 'u8'; 893 | }, 894 | ]; 895 | }; 896 | }, 897 | ]; 898 | types: [ 899 | { 900 | name: 'VaultBumps'; 901 | type: { 902 | kind: 'struct'; 903 | fields: [ 904 | { 905 | name: 'vaultBump'; 906 | type: 'u8'; 907 | }, 908 | { 909 | name: 'tokenVaultBump'; 910 | type: 'u8'; 911 | }, 912 | ]; 913 | }; 914 | }, 915 | { 916 | name: 'StrategyBumps'; 917 | type: { 918 | kind: 'struct'; 919 | fields: [ 920 | { 921 | name: 'strategyIndex'; 922 | type: 'u8'; 923 | }, 924 | { 925 | name: 'otherBumps'; 926 | type: { 927 | array: ['u8', 10]; 928 | }; 929 | }, 930 | ]; 931 | }; 932 | }, 933 | { 934 | name: 'LockedProfitTracker'; 935 | type: { 936 | kind: 'struct'; 937 | fields: [ 938 | { 939 | name: 'lastUpdatedLockedProfit'; 940 | type: 'u64'; 941 | }, 942 | { 943 | name: 'lastReport'; 944 | type: 'u64'; 945 | }, 946 | { 947 | name: 'lockedProfitDegradation'; 948 | type: 'u64'; 949 | }, 950 | ]; 951 | }; 952 | }, 953 | { 954 | name: 'StrategyType'; 955 | type: { 956 | kind: 'enum'; 957 | variants: [ 958 | { 959 | name: 'PortFinanceWithoutLM'; 960 | }, 961 | { 962 | name: 'PortFinanceWithLM'; 963 | }, 964 | { 965 | name: 'SolendWithoutLM'; 966 | }, 967 | { 968 | name: 'Mango'; 969 | }, 970 | { 971 | name: 'SolendWithLM'; 972 | }, 973 | { 974 | name: 'ApricotWithoutLM'; 975 | }, 976 | { 977 | name: 'Francium'; 978 | }, 979 | { 980 | name: 'Tulip'; 981 | }, 982 | { 983 | name: 'Vault'; 984 | }, 985 | { 986 | name: 'Drift'; 987 | }, 988 | { 989 | name: 'Frakt'; 990 | }, 991 | { 992 | name: 'Marginfi'; 993 | }, 994 | ]; 995 | }; 996 | }, 997 | ]; 998 | events: [ 999 | { 1000 | name: 'AddLiquidity'; 1001 | fields: [ 1002 | { 1003 | name: 'lpMintAmount'; 1004 | type: 'u64'; 1005 | index: false; 1006 | }, 1007 | { 1008 | name: 'tokenAmount'; 1009 | type: 'u64'; 1010 | index: false; 1011 | }, 1012 | ]; 1013 | }, 1014 | { 1015 | name: 'RemoveLiquidity'; 1016 | fields: [ 1017 | { 1018 | name: 'lpUnmintAmount'; 1019 | type: 'u64'; 1020 | index: false; 1021 | }, 1022 | { 1023 | name: 'tokenAmount'; 1024 | type: 'u64'; 1025 | index: false; 1026 | }, 1027 | ]; 1028 | }, 1029 | { 1030 | name: 'StrategyDeposit'; 1031 | fields: [ 1032 | { 1033 | name: 'strategyType'; 1034 | type: { 1035 | defined: 'StrategyType'; 1036 | }; 1037 | index: false; 1038 | }, 1039 | { 1040 | name: 'tokenAmount'; 1041 | type: 'u64'; 1042 | index: false; 1043 | }, 1044 | ]; 1045 | }, 1046 | { 1047 | name: 'StrategyWithdraw'; 1048 | fields: [ 1049 | { 1050 | name: 'strategyType'; 1051 | type: { 1052 | defined: 'StrategyType'; 1053 | }; 1054 | index: false; 1055 | }, 1056 | { 1057 | name: 'collateralAmount'; 1058 | type: 'u64'; 1059 | index: false; 1060 | }, 1061 | { 1062 | name: 'estimatedTokenAmount'; 1063 | type: 'u64'; 1064 | index: false; 1065 | }, 1066 | ]; 1067 | }, 1068 | { 1069 | name: 'ClaimReward'; 1070 | fields: [ 1071 | { 1072 | name: 'strategyType'; 1073 | type: { 1074 | defined: 'StrategyType'; 1075 | }; 1076 | index: false; 1077 | }, 1078 | { 1079 | name: 'tokenAmount'; 1080 | type: 'u64'; 1081 | index: false; 1082 | }, 1083 | { 1084 | name: 'mintAccount'; 1085 | type: 'publicKey'; 1086 | index: false; 1087 | }, 1088 | ]; 1089 | }, 1090 | { 1091 | name: 'PerformanceFee'; 1092 | fields: [ 1093 | { 1094 | name: 'lpMintMore'; 1095 | type: 'u64'; 1096 | index: false; 1097 | }, 1098 | ]; 1099 | }, 1100 | { 1101 | name: 'ReportLoss'; 1102 | fields: [ 1103 | { 1104 | name: 'strategy'; 1105 | type: 'publicKey'; 1106 | index: false; 1107 | }, 1108 | { 1109 | name: 'loss'; 1110 | type: 'u64'; 1111 | index: false; 1112 | }, 1113 | ]; 1114 | }, 1115 | { 1116 | name: 'TotalAmount'; 1117 | fields: [ 1118 | { 1119 | name: 'totalAmount'; 1120 | type: 'u64'; 1121 | index: false; 1122 | }, 1123 | ]; 1124 | }, 1125 | ]; 1126 | errors: [ 1127 | { 1128 | code: 6000; 1129 | name: 'VaultIsDisabled'; 1130 | msg: 'Vault is disabled'; 1131 | }, 1132 | { 1133 | code: 6001; 1134 | name: 'ExceededSlippage'; 1135 | msg: 'Exceeded slippage tolerance'; 1136 | }, 1137 | { 1138 | code: 6002; 1139 | name: 'StrategyIsNotExisted'; 1140 | msg: 'Strategy is not existed'; 1141 | }, 1142 | { 1143 | code: 6003; 1144 | name: 'UnAuthorized'; 1145 | msg: 'UnAuthorized'; 1146 | }, 1147 | { 1148 | code: 6004; 1149 | name: 'MathOverflow'; 1150 | msg: 'Math operation overflow'; 1151 | }, 1152 | { 1153 | code: 6005; 1154 | name: 'ProtocolIsNotSupported'; 1155 | msg: 'Protocol is not supported'; 1156 | }, 1157 | { 1158 | code: 6006; 1159 | name: 'UnMatchReserve'; 1160 | msg: 'Reserve does not support token mint'; 1161 | }, 1162 | { 1163 | code: 6007; 1164 | name: 'InvalidLockedProfitDegradation'; 1165 | msg: 'lockedProfitDegradation is invalid'; 1166 | }, 1167 | { 1168 | code: 6008; 1169 | name: 'MaxStrategyReached'; 1170 | msg: 'Maximum number of strategies have been reached'; 1171 | }, 1172 | { 1173 | code: 6009; 1174 | name: 'StrategyExisted'; 1175 | msg: 'Strategy existed'; 1176 | }, 1177 | { 1178 | code: 6010; 1179 | name: 'InvalidUnmintAmount'; 1180 | msg: 'Invalid unmint amount'; 1181 | }, 1182 | { 1183 | code: 6011; 1184 | name: 'InvalidAccountsForStrategy'; 1185 | msg: 'Invalid accounts for strategy'; 1186 | }, 1187 | { 1188 | code: 6012; 1189 | name: 'InvalidBump'; 1190 | msg: 'Invalid bump'; 1191 | }, 1192 | { 1193 | code: 6013; 1194 | name: 'AmountMustGreaterThanZero'; 1195 | msg: 'Amount must be greater than 0'; 1196 | }, 1197 | { 1198 | code: 6014; 1199 | name: 'MangoIsNotSupportedAnymore'; 1200 | msg: 'Mango is not supported anymore'; 1201 | }, 1202 | { 1203 | code: 6015; 1204 | name: 'StrategyIsNotSupported'; 1205 | msg: 'Strategy is not supported'; 1206 | }, 1207 | { 1208 | code: 6016; 1209 | name: 'PayAmountIsExeeced'; 1210 | msg: 'Pay amount is exceeded'; 1211 | }, 1212 | { 1213 | code: 6017; 1214 | name: 'FeeVaultIsNotSet'; 1215 | msg: 'Fee vault is not set'; 1216 | }, 1217 | { 1218 | code: 6018; 1219 | name: 'LendingAssertionViolation'; 1220 | msg: 'deposit amount in lending is not matched'; 1221 | }, 1222 | { 1223 | code: 6019; 1224 | name: 'HaveMoneyInLending'; 1225 | msg: 'Cannot remove strategy becase we have some in lending'; 1226 | }, 1227 | ]; 1228 | }; 1229 | 1230 | export const IDL: Vault = { 1231 | version: '0.7.2', 1232 | name: 'vault', 1233 | instructions: [ 1234 | { 1235 | name: 'initialize', 1236 | accounts: [ 1237 | { 1238 | name: 'vault', 1239 | isMut: true, 1240 | isSigner: false, 1241 | }, 1242 | { 1243 | name: 'payer', 1244 | isMut: true, 1245 | isSigner: true, 1246 | }, 1247 | { 1248 | name: 'tokenVault', 1249 | isMut: true, 1250 | isSigner: false, 1251 | }, 1252 | { 1253 | name: 'tokenMint', 1254 | isMut: false, 1255 | isSigner: false, 1256 | }, 1257 | { 1258 | name: 'lpMint', 1259 | isMut: true, 1260 | isSigner: false, 1261 | }, 1262 | { 1263 | name: 'rent', 1264 | isMut: false, 1265 | isSigner: false, 1266 | }, 1267 | { 1268 | name: 'tokenProgram', 1269 | isMut: false, 1270 | isSigner: false, 1271 | }, 1272 | { 1273 | name: 'systemProgram', 1274 | isMut: false, 1275 | isSigner: false, 1276 | }, 1277 | ], 1278 | args: [], 1279 | }, 1280 | { 1281 | name: 'enableVault', 1282 | accounts: [ 1283 | { 1284 | name: 'vault', 1285 | isMut: true, 1286 | isSigner: false, 1287 | }, 1288 | { 1289 | name: 'admin', 1290 | isMut: false, 1291 | isSigner: true, 1292 | }, 1293 | ], 1294 | args: [ 1295 | { 1296 | name: 'enabled', 1297 | type: 'u8', 1298 | }, 1299 | ], 1300 | }, 1301 | { 1302 | name: 'setOperator', 1303 | accounts: [ 1304 | { 1305 | name: 'vault', 1306 | isMut: true, 1307 | isSigner: false, 1308 | }, 1309 | { 1310 | name: 'operator', 1311 | isMut: false, 1312 | isSigner: false, 1313 | }, 1314 | { 1315 | name: 'admin', 1316 | isMut: false, 1317 | isSigner: true, 1318 | }, 1319 | ], 1320 | args: [], 1321 | }, 1322 | { 1323 | name: 'updateLockedProfitDegradation', 1324 | accounts: [ 1325 | { 1326 | name: 'vault', 1327 | isMut: true, 1328 | isSigner: false, 1329 | }, 1330 | { 1331 | name: 'admin', 1332 | isMut: false, 1333 | isSigner: true, 1334 | }, 1335 | ], 1336 | args: [ 1337 | { 1338 | name: 'lockedProfitDegradation', 1339 | type: 'u64', 1340 | }, 1341 | ], 1342 | }, 1343 | { 1344 | name: 'getUnlockedAmount', 1345 | accounts: [ 1346 | { 1347 | name: 'vault', 1348 | isMut: false, 1349 | isSigner: false, 1350 | }, 1351 | ], 1352 | args: [], 1353 | }, 1354 | { 1355 | name: 'transferAdmin', 1356 | accounts: [ 1357 | { 1358 | name: 'vault', 1359 | isMut: true, 1360 | isSigner: false, 1361 | }, 1362 | { 1363 | name: 'admin', 1364 | isMut: false, 1365 | isSigner: true, 1366 | }, 1367 | { 1368 | name: 'newAdmin', 1369 | isMut: false, 1370 | isSigner: true, 1371 | }, 1372 | ], 1373 | args: [], 1374 | }, 1375 | { 1376 | name: 'transferFeeVault', 1377 | accounts: [ 1378 | { 1379 | name: 'vault', 1380 | isMut: true, 1381 | isSigner: false, 1382 | }, 1383 | { 1384 | name: 'admin', 1385 | isMut: false, 1386 | isSigner: true, 1387 | }, 1388 | { 1389 | name: 'newFeeVault', 1390 | isMut: false, 1391 | isSigner: false, 1392 | }, 1393 | ], 1394 | args: [], 1395 | }, 1396 | { 1397 | name: 'initializeStrategy', 1398 | accounts: [ 1399 | { 1400 | name: 'vault', 1401 | isMut: true, 1402 | isSigner: false, 1403 | }, 1404 | { 1405 | name: 'strategyProgram', 1406 | isMut: false, 1407 | isSigner: false, 1408 | }, 1409 | { 1410 | name: 'strategy', 1411 | isMut: true, 1412 | isSigner: false, 1413 | }, 1414 | { 1415 | name: 'reserve', 1416 | isMut: true, 1417 | isSigner: false, 1418 | }, 1419 | { 1420 | name: 'collateralVault', 1421 | isMut: true, 1422 | isSigner: false, 1423 | }, 1424 | { 1425 | name: 'collateralMint', 1426 | isMut: false, 1427 | isSigner: false, 1428 | }, 1429 | { 1430 | name: 'admin', 1431 | isMut: true, 1432 | isSigner: true, 1433 | }, 1434 | { 1435 | name: 'systemProgram', 1436 | isMut: false, 1437 | isSigner: false, 1438 | }, 1439 | { 1440 | name: 'rent', 1441 | isMut: false, 1442 | isSigner: false, 1443 | }, 1444 | { 1445 | name: 'tokenProgram', 1446 | isMut: false, 1447 | isSigner: false, 1448 | }, 1449 | ], 1450 | args: [ 1451 | { 1452 | name: 'bumps', 1453 | type: { 1454 | defined: 'StrategyBumps', 1455 | }, 1456 | }, 1457 | { 1458 | name: 'strategyType', 1459 | type: { 1460 | defined: 'StrategyType', 1461 | }, 1462 | }, 1463 | ], 1464 | }, 1465 | { 1466 | name: 'removeStrategy', 1467 | accounts: [ 1468 | { 1469 | name: 'vault', 1470 | isMut: true, 1471 | isSigner: false, 1472 | }, 1473 | { 1474 | name: 'strategy', 1475 | isMut: true, 1476 | isSigner: false, 1477 | }, 1478 | { 1479 | name: 'strategyProgram', 1480 | isMut: false, 1481 | isSigner: false, 1482 | }, 1483 | { 1484 | name: 'collateralVault', 1485 | isMut: true, 1486 | isSigner: false, 1487 | }, 1488 | { 1489 | name: 'reserve', 1490 | isMut: true, 1491 | isSigner: false, 1492 | }, 1493 | { 1494 | name: 'tokenVault', 1495 | isMut: true, 1496 | isSigner: false, 1497 | }, 1498 | { 1499 | name: 'feeVault', 1500 | isMut: true, 1501 | isSigner: false, 1502 | }, 1503 | { 1504 | name: 'lpMint', 1505 | isMut: true, 1506 | isSigner: false, 1507 | }, 1508 | { 1509 | name: 'tokenProgram', 1510 | isMut: false, 1511 | isSigner: false, 1512 | }, 1513 | { 1514 | name: 'admin', 1515 | isMut: false, 1516 | isSigner: true, 1517 | }, 1518 | ], 1519 | args: [], 1520 | }, 1521 | { 1522 | name: 'removeStrategy2', 1523 | accounts: [ 1524 | { 1525 | name: 'vault', 1526 | isMut: true, 1527 | isSigner: false, 1528 | }, 1529 | { 1530 | name: 'strategy', 1531 | isMut: true, 1532 | isSigner: false, 1533 | }, 1534 | { 1535 | name: 'strategyProgram', 1536 | isMut: false, 1537 | isSigner: false, 1538 | }, 1539 | { 1540 | name: 'collateralVault', 1541 | isMut: true, 1542 | isSigner: false, 1543 | }, 1544 | { 1545 | name: 'reserve', 1546 | isMut: true, 1547 | isSigner: false, 1548 | }, 1549 | { 1550 | name: 'tokenVault', 1551 | isMut: true, 1552 | isSigner: false, 1553 | }, 1554 | { 1555 | name: 'tokenAdminAdvancePayment', 1556 | isMut: true, 1557 | isSigner: false, 1558 | }, 1559 | { 1560 | name: 'tokenVaultAdvancePayment', 1561 | isMut: true, 1562 | isSigner: false, 1563 | }, 1564 | { 1565 | name: 'feeVault', 1566 | isMut: true, 1567 | isSigner: false, 1568 | }, 1569 | { 1570 | name: 'lpMint', 1571 | isMut: true, 1572 | isSigner: false, 1573 | }, 1574 | { 1575 | name: 'tokenProgram', 1576 | isMut: false, 1577 | isSigner: false, 1578 | }, 1579 | { 1580 | name: 'admin', 1581 | isMut: false, 1582 | isSigner: true, 1583 | }, 1584 | ], 1585 | args: [ 1586 | { 1587 | name: 'maxAdminPayAmount', 1588 | type: 'u64', 1589 | }, 1590 | ], 1591 | }, 1592 | { 1593 | name: 'collectDust', 1594 | accounts: [ 1595 | { 1596 | name: 'vault', 1597 | isMut: false, 1598 | isSigner: false, 1599 | }, 1600 | { 1601 | name: 'tokenVault', 1602 | isMut: true, 1603 | isSigner: false, 1604 | }, 1605 | { 1606 | name: 'tokenAdmin', 1607 | isMut: true, 1608 | isSigner: false, 1609 | }, 1610 | { 1611 | name: 'admin', 1612 | isMut: false, 1613 | isSigner: true, 1614 | }, 1615 | { 1616 | name: 'tokenProgram', 1617 | isMut: false, 1618 | isSigner: false, 1619 | }, 1620 | ], 1621 | args: [], 1622 | }, 1623 | { 1624 | name: 'addStrategy', 1625 | accounts: [ 1626 | { 1627 | name: 'vault', 1628 | isMut: true, 1629 | isSigner: false, 1630 | }, 1631 | { 1632 | name: 'strategy', 1633 | isMut: false, 1634 | isSigner: false, 1635 | }, 1636 | { 1637 | name: 'admin', 1638 | isMut: false, 1639 | isSigner: true, 1640 | }, 1641 | ], 1642 | args: [], 1643 | }, 1644 | { 1645 | name: 'depositStrategy', 1646 | accounts: [ 1647 | { 1648 | name: 'vault', 1649 | isMut: true, 1650 | isSigner: false, 1651 | }, 1652 | { 1653 | name: 'strategy', 1654 | isMut: true, 1655 | isSigner: false, 1656 | }, 1657 | { 1658 | name: 'tokenVault', 1659 | isMut: true, 1660 | isSigner: false, 1661 | }, 1662 | { 1663 | name: 'feeVault', 1664 | isMut: true, 1665 | isSigner: false, 1666 | }, 1667 | { 1668 | name: 'lpMint', 1669 | isMut: true, 1670 | isSigner: false, 1671 | }, 1672 | { 1673 | name: 'strategyProgram', 1674 | isMut: false, 1675 | isSigner: false, 1676 | }, 1677 | { 1678 | name: 'collateralVault', 1679 | isMut: true, 1680 | isSigner: false, 1681 | }, 1682 | { 1683 | name: 'reserve', 1684 | isMut: true, 1685 | isSigner: false, 1686 | }, 1687 | { 1688 | name: 'tokenProgram', 1689 | isMut: false, 1690 | isSigner: false, 1691 | }, 1692 | { 1693 | name: 'operator', 1694 | isMut: false, 1695 | isSigner: true, 1696 | }, 1697 | ], 1698 | args: [ 1699 | { 1700 | name: 'amount', 1701 | type: 'u64', 1702 | }, 1703 | ], 1704 | }, 1705 | { 1706 | name: 'withdrawStrategy', 1707 | accounts: [ 1708 | { 1709 | name: 'vault', 1710 | isMut: true, 1711 | isSigner: false, 1712 | }, 1713 | { 1714 | name: 'strategy', 1715 | isMut: true, 1716 | isSigner: false, 1717 | }, 1718 | { 1719 | name: 'tokenVault', 1720 | isMut: true, 1721 | isSigner: false, 1722 | }, 1723 | { 1724 | name: 'feeVault', 1725 | isMut: true, 1726 | isSigner: false, 1727 | }, 1728 | { 1729 | name: 'lpMint', 1730 | isMut: true, 1731 | isSigner: false, 1732 | }, 1733 | { 1734 | name: 'strategyProgram', 1735 | isMut: false, 1736 | isSigner: false, 1737 | }, 1738 | { 1739 | name: 'collateralVault', 1740 | isMut: true, 1741 | isSigner: false, 1742 | }, 1743 | { 1744 | name: 'reserve', 1745 | isMut: true, 1746 | isSigner: false, 1747 | }, 1748 | { 1749 | name: 'tokenProgram', 1750 | isMut: false, 1751 | isSigner: false, 1752 | }, 1753 | { 1754 | name: 'operator', 1755 | isMut: false, 1756 | isSigner: true, 1757 | }, 1758 | ], 1759 | args: [ 1760 | { 1761 | name: 'amount', 1762 | type: 'u64', 1763 | }, 1764 | ], 1765 | }, 1766 | { 1767 | name: 'claimRewards', 1768 | accounts: [ 1769 | { 1770 | name: 'vault', 1771 | isMut: false, 1772 | isSigner: false, 1773 | }, 1774 | { 1775 | name: 'strategy', 1776 | isMut: false, 1777 | isSigner: false, 1778 | }, 1779 | { 1780 | name: 'tokenProgram', 1781 | isMut: false, 1782 | isSigner: false, 1783 | }, 1784 | { 1785 | name: 'tokenRewardAcc', 1786 | isMut: true, 1787 | isSigner: false, 1788 | }, 1789 | { 1790 | name: 'operator', 1791 | isMut: false, 1792 | isSigner: true, 1793 | }, 1794 | ], 1795 | args: [], 1796 | }, 1797 | { 1798 | name: 'withdraw2', 1799 | accounts: [ 1800 | { 1801 | name: 'vault', 1802 | isMut: true, 1803 | isSigner: false, 1804 | }, 1805 | { 1806 | name: 'tokenVault', 1807 | isMut: true, 1808 | isSigner: false, 1809 | }, 1810 | { 1811 | name: 'lpMint', 1812 | isMut: true, 1813 | isSigner: false, 1814 | }, 1815 | { 1816 | name: 'userToken', 1817 | isMut: true, 1818 | isSigner: false, 1819 | }, 1820 | { 1821 | name: 'userLp', 1822 | isMut: true, 1823 | isSigner: false, 1824 | }, 1825 | { 1826 | name: 'user', 1827 | isMut: false, 1828 | isSigner: true, 1829 | }, 1830 | { 1831 | name: 'tokenProgram', 1832 | isMut: false, 1833 | isSigner: false, 1834 | }, 1835 | ], 1836 | args: [ 1837 | { 1838 | name: 'unmintAmount', 1839 | type: 'u64', 1840 | }, 1841 | { 1842 | name: 'minOutAmount', 1843 | type: 'u64', 1844 | }, 1845 | ], 1846 | }, 1847 | { 1848 | name: 'deposit', 1849 | accounts: [ 1850 | { 1851 | name: 'vault', 1852 | isMut: true, 1853 | isSigner: false, 1854 | }, 1855 | { 1856 | name: 'tokenVault', 1857 | isMut: true, 1858 | isSigner: false, 1859 | }, 1860 | { 1861 | name: 'lpMint', 1862 | isMut: true, 1863 | isSigner: false, 1864 | }, 1865 | { 1866 | name: 'userToken', 1867 | isMut: true, 1868 | isSigner: false, 1869 | }, 1870 | { 1871 | name: 'userLp', 1872 | isMut: true, 1873 | isSigner: false, 1874 | }, 1875 | { 1876 | name: 'user', 1877 | isMut: false, 1878 | isSigner: true, 1879 | }, 1880 | { 1881 | name: 'tokenProgram', 1882 | isMut: false, 1883 | isSigner: false, 1884 | }, 1885 | ], 1886 | args: [ 1887 | { 1888 | name: 'tokenAmount', 1889 | type: 'u64', 1890 | }, 1891 | { 1892 | name: 'minimumLpTokenAmount', 1893 | type: 'u64', 1894 | }, 1895 | ], 1896 | }, 1897 | { 1898 | name: 'withdraw', 1899 | accounts: [ 1900 | { 1901 | name: 'vault', 1902 | isMut: true, 1903 | isSigner: false, 1904 | }, 1905 | { 1906 | name: 'tokenVault', 1907 | isMut: true, 1908 | isSigner: false, 1909 | }, 1910 | { 1911 | name: 'lpMint', 1912 | isMut: true, 1913 | isSigner: false, 1914 | }, 1915 | { 1916 | name: 'userToken', 1917 | isMut: true, 1918 | isSigner: false, 1919 | }, 1920 | { 1921 | name: 'userLp', 1922 | isMut: true, 1923 | isSigner: false, 1924 | }, 1925 | { 1926 | name: 'user', 1927 | isMut: false, 1928 | isSigner: true, 1929 | }, 1930 | { 1931 | name: 'tokenProgram', 1932 | isMut: false, 1933 | isSigner: false, 1934 | }, 1935 | ], 1936 | args: [ 1937 | { 1938 | name: 'unmintAmount', 1939 | type: 'u64', 1940 | }, 1941 | { 1942 | name: 'minOutAmount', 1943 | type: 'u64', 1944 | }, 1945 | ], 1946 | }, 1947 | { 1948 | name: 'withdrawDirectlyFromStrategy', 1949 | accounts: [ 1950 | { 1951 | name: 'vault', 1952 | isMut: true, 1953 | isSigner: false, 1954 | }, 1955 | { 1956 | name: 'strategy', 1957 | isMut: true, 1958 | isSigner: false, 1959 | }, 1960 | { 1961 | name: 'reserve', 1962 | isMut: true, 1963 | isSigner: false, 1964 | }, 1965 | { 1966 | name: 'strategyProgram', 1967 | isMut: false, 1968 | isSigner: false, 1969 | }, 1970 | { 1971 | name: 'collateralVault', 1972 | isMut: true, 1973 | isSigner: false, 1974 | }, 1975 | { 1976 | name: 'tokenVault', 1977 | isMut: true, 1978 | isSigner: false, 1979 | }, 1980 | { 1981 | name: 'lpMint', 1982 | isMut: true, 1983 | isSigner: false, 1984 | }, 1985 | { 1986 | name: 'feeVault', 1987 | isMut: true, 1988 | isSigner: false, 1989 | }, 1990 | { 1991 | name: 'userToken', 1992 | isMut: true, 1993 | isSigner: false, 1994 | }, 1995 | { 1996 | name: 'userLp', 1997 | isMut: true, 1998 | isSigner: false, 1999 | }, 2000 | { 2001 | name: 'user', 2002 | isMut: false, 2003 | isSigner: true, 2004 | }, 2005 | { 2006 | name: 'tokenProgram', 2007 | isMut: false, 2008 | isSigner: false, 2009 | }, 2010 | ], 2011 | args: [ 2012 | { 2013 | name: 'unmintAmount', 2014 | type: 'u64', 2015 | }, 2016 | { 2017 | name: 'minOutAmount', 2018 | type: 'u64', 2019 | }, 2020 | ], 2021 | }, 2022 | ], 2023 | accounts: [ 2024 | { 2025 | name: 'vault', 2026 | type: { 2027 | kind: 'struct', 2028 | fields: [ 2029 | { 2030 | name: 'enabled', 2031 | type: 'u8', 2032 | }, 2033 | { 2034 | name: 'bumps', 2035 | type: { 2036 | defined: 'VaultBumps', 2037 | }, 2038 | }, 2039 | { 2040 | name: 'totalAmount', 2041 | type: 'u64', 2042 | }, 2043 | { 2044 | name: 'tokenVault', 2045 | type: 'publicKey', 2046 | }, 2047 | { 2048 | name: 'feeVault', 2049 | type: 'publicKey', 2050 | }, 2051 | { 2052 | name: 'tokenMint', 2053 | type: 'publicKey', 2054 | }, 2055 | { 2056 | name: 'lpMint', 2057 | type: 'publicKey', 2058 | }, 2059 | { 2060 | name: 'strategies', 2061 | type: { 2062 | array: ['publicKey', 30], 2063 | }, 2064 | }, 2065 | { 2066 | name: 'base', 2067 | type: 'publicKey', 2068 | }, 2069 | { 2070 | name: 'admin', 2071 | type: 'publicKey', 2072 | }, 2073 | { 2074 | name: 'operator', 2075 | type: 'publicKey', 2076 | }, 2077 | { 2078 | name: 'lockedProfitTracker', 2079 | type: { 2080 | defined: 'LockedProfitTracker', 2081 | }, 2082 | }, 2083 | ], 2084 | }, 2085 | }, 2086 | { 2087 | name: 'strategy', 2088 | type: { 2089 | kind: 'struct', 2090 | fields: [ 2091 | { 2092 | name: 'reserve', 2093 | type: 'publicKey', 2094 | }, 2095 | { 2096 | name: 'collateralVault', 2097 | type: 'publicKey', 2098 | }, 2099 | { 2100 | name: 'strategyType', 2101 | type: { 2102 | defined: 'StrategyType', 2103 | }, 2104 | }, 2105 | { 2106 | name: 'currentLiquidity', 2107 | type: 'u64', 2108 | }, 2109 | { 2110 | name: 'bumps', 2111 | type: { 2112 | array: ['u8', 10], 2113 | }, 2114 | }, 2115 | { 2116 | name: 'vault', 2117 | type: 'publicKey', 2118 | }, 2119 | { 2120 | name: 'isDisable', 2121 | type: 'u8', 2122 | }, 2123 | ], 2124 | }, 2125 | }, 2126 | ], 2127 | types: [ 2128 | { 2129 | name: 'VaultBumps', 2130 | type: { 2131 | kind: 'struct', 2132 | fields: [ 2133 | { 2134 | name: 'vaultBump', 2135 | type: 'u8', 2136 | }, 2137 | { 2138 | name: 'tokenVaultBump', 2139 | type: 'u8', 2140 | }, 2141 | ], 2142 | }, 2143 | }, 2144 | { 2145 | name: 'StrategyBumps', 2146 | type: { 2147 | kind: 'struct', 2148 | fields: [ 2149 | { 2150 | name: 'strategyIndex', 2151 | type: 'u8', 2152 | }, 2153 | { 2154 | name: 'otherBumps', 2155 | type: { 2156 | array: ['u8', 10], 2157 | }, 2158 | }, 2159 | ], 2160 | }, 2161 | }, 2162 | { 2163 | name: 'LockedProfitTracker', 2164 | type: { 2165 | kind: 'struct', 2166 | fields: [ 2167 | { 2168 | name: 'lastUpdatedLockedProfit', 2169 | type: 'u64', 2170 | }, 2171 | { 2172 | name: 'lastReport', 2173 | type: 'u64', 2174 | }, 2175 | { 2176 | name: 'lockedProfitDegradation', 2177 | type: 'u64', 2178 | }, 2179 | ], 2180 | }, 2181 | }, 2182 | { 2183 | name: 'StrategyType', 2184 | type: { 2185 | kind: 'enum', 2186 | variants: [ 2187 | { 2188 | name: 'PortFinanceWithoutLM', 2189 | }, 2190 | { 2191 | name: 'PortFinanceWithLM', 2192 | }, 2193 | { 2194 | name: 'SolendWithoutLM', 2195 | }, 2196 | { 2197 | name: 'Mango', 2198 | }, 2199 | { 2200 | name: 'SolendWithLM', 2201 | }, 2202 | { 2203 | name: 'ApricotWithoutLM', 2204 | }, 2205 | { 2206 | name: 'Francium', 2207 | }, 2208 | { 2209 | name: 'Tulip', 2210 | }, 2211 | { 2212 | name: 'Vault', 2213 | }, 2214 | { 2215 | name: 'Drift', 2216 | }, 2217 | { 2218 | name: 'Frakt', 2219 | }, 2220 | { 2221 | name: 'Marginfi', 2222 | }, 2223 | ], 2224 | }, 2225 | }, 2226 | ], 2227 | events: [ 2228 | { 2229 | name: 'AddLiquidity', 2230 | fields: [ 2231 | { 2232 | name: 'lpMintAmount', 2233 | type: 'u64', 2234 | index: false, 2235 | }, 2236 | { 2237 | name: 'tokenAmount', 2238 | type: 'u64', 2239 | index: false, 2240 | }, 2241 | ], 2242 | }, 2243 | { 2244 | name: 'RemoveLiquidity', 2245 | fields: [ 2246 | { 2247 | name: 'lpUnmintAmount', 2248 | type: 'u64', 2249 | index: false, 2250 | }, 2251 | { 2252 | name: 'tokenAmount', 2253 | type: 'u64', 2254 | index: false, 2255 | }, 2256 | ], 2257 | }, 2258 | { 2259 | name: 'StrategyDeposit', 2260 | fields: [ 2261 | { 2262 | name: 'strategyType', 2263 | type: { 2264 | defined: 'StrategyType', 2265 | }, 2266 | index: false, 2267 | }, 2268 | { 2269 | name: 'tokenAmount', 2270 | type: 'u64', 2271 | index: false, 2272 | }, 2273 | ], 2274 | }, 2275 | { 2276 | name: 'StrategyWithdraw', 2277 | fields: [ 2278 | { 2279 | name: 'strategyType', 2280 | type: { 2281 | defined: 'StrategyType', 2282 | }, 2283 | index: false, 2284 | }, 2285 | { 2286 | name: 'collateralAmount', 2287 | type: 'u64', 2288 | index: false, 2289 | }, 2290 | { 2291 | name: 'estimatedTokenAmount', 2292 | type: 'u64', 2293 | index: false, 2294 | }, 2295 | ], 2296 | }, 2297 | { 2298 | name: 'ClaimReward', 2299 | fields: [ 2300 | { 2301 | name: 'strategyType', 2302 | type: { 2303 | defined: 'StrategyType', 2304 | }, 2305 | index: false, 2306 | }, 2307 | { 2308 | name: 'tokenAmount', 2309 | type: 'u64', 2310 | index: false, 2311 | }, 2312 | { 2313 | name: 'mintAccount', 2314 | type: 'publicKey', 2315 | index: false, 2316 | }, 2317 | ], 2318 | }, 2319 | { 2320 | name: 'PerformanceFee', 2321 | fields: [ 2322 | { 2323 | name: 'lpMintMore', 2324 | type: 'u64', 2325 | index: false, 2326 | }, 2327 | ], 2328 | }, 2329 | { 2330 | name: 'ReportLoss', 2331 | fields: [ 2332 | { 2333 | name: 'strategy', 2334 | type: 'publicKey', 2335 | index: false, 2336 | }, 2337 | { 2338 | name: 'loss', 2339 | type: 'u64', 2340 | index: false, 2341 | }, 2342 | ], 2343 | }, 2344 | { 2345 | name: 'TotalAmount', 2346 | fields: [ 2347 | { 2348 | name: 'totalAmount', 2349 | type: 'u64', 2350 | index: false, 2351 | }, 2352 | ], 2353 | }, 2354 | ], 2355 | errors: [ 2356 | { 2357 | code: 6000, 2358 | name: 'VaultIsDisabled', 2359 | msg: 'Vault is disabled', 2360 | }, 2361 | { 2362 | code: 6001, 2363 | name: 'ExceededSlippage', 2364 | msg: 'Exceeded slippage tolerance', 2365 | }, 2366 | { 2367 | code: 6002, 2368 | name: 'StrategyIsNotExisted', 2369 | msg: 'Strategy is not existed', 2370 | }, 2371 | { 2372 | code: 6003, 2373 | name: 'UnAuthorized', 2374 | msg: 'UnAuthorized', 2375 | }, 2376 | { 2377 | code: 6004, 2378 | name: 'MathOverflow', 2379 | msg: 'Math operation overflow', 2380 | }, 2381 | { 2382 | code: 6005, 2383 | name: 'ProtocolIsNotSupported', 2384 | msg: 'Protocol is not supported', 2385 | }, 2386 | { 2387 | code: 6006, 2388 | name: 'UnMatchReserve', 2389 | msg: 'Reserve does not support token mint', 2390 | }, 2391 | { 2392 | code: 6007, 2393 | name: 'InvalidLockedProfitDegradation', 2394 | msg: 'lockedProfitDegradation is invalid', 2395 | }, 2396 | { 2397 | code: 6008, 2398 | name: 'MaxStrategyReached', 2399 | msg: 'Maximum number of strategies have been reached', 2400 | }, 2401 | { 2402 | code: 6009, 2403 | name: 'StrategyExisted', 2404 | msg: 'Strategy existed', 2405 | }, 2406 | { 2407 | code: 6010, 2408 | name: 'InvalidUnmintAmount', 2409 | msg: 'Invalid unmint amount', 2410 | }, 2411 | { 2412 | code: 6011, 2413 | name: 'InvalidAccountsForStrategy', 2414 | msg: 'Invalid accounts for strategy', 2415 | }, 2416 | { 2417 | code: 6012, 2418 | name: 'InvalidBump', 2419 | msg: 'Invalid bump', 2420 | }, 2421 | { 2422 | code: 6013, 2423 | name: 'AmountMustGreaterThanZero', 2424 | msg: 'Amount must be greater than 0', 2425 | }, 2426 | { 2427 | code: 6014, 2428 | name: 'MangoIsNotSupportedAnymore', 2429 | msg: 'Mango is not supported anymore', 2430 | }, 2431 | { 2432 | code: 6015, 2433 | name: 'StrategyIsNotSupported', 2434 | msg: 'Strategy is not supported', 2435 | }, 2436 | { 2437 | code: 6016, 2438 | name: 'PayAmountIsExeeced', 2439 | msg: 'Pay amount is exceeded', 2440 | }, 2441 | { 2442 | code: 6017, 2443 | name: 'FeeVaultIsNotSet', 2444 | msg: 'Fee vault is not set', 2445 | }, 2446 | { 2447 | code: 6018, 2448 | name: 'LendingAssertionViolation', 2449 | msg: 'deposit amount in lending is not matched', 2450 | }, 2451 | { 2452 | code: 6019, 2453 | name: 'HaveMoneyInLending', 2454 | msg: 'Cannot remove strategy becase we have some in lending', 2455 | }, 2456 | ], 2457 | }; 2458 | -------------------------------------------------------------------------------- /ts-client/src/vault/index.ts: -------------------------------------------------------------------------------- 1 | import { AnchorProvider, Program, BN } from '@coral-xyz/anchor'; 2 | import { 3 | PublicKey, 4 | TransactionInstruction, 5 | Connection, 6 | Transaction, 7 | Cluster, 8 | SYSVAR_RENT_PUBKEY, 9 | SystemProgram, 10 | } from '@solana/web3.js'; 11 | import { TOKEN_PROGRAM_ID, NATIVE_MINT, getMint, Mint, unpackMint } from '@solana/spl-token'; 12 | 13 | import { 14 | AffiliateInfo, 15 | AffiliateVaultProgram, 16 | VaultImplementation, 17 | VaultProgram, 18 | VaultState, 19 | VaultStateAndLp, 20 | } from './types'; 21 | import { 22 | chunkedFetchMultipleVaultAccount, 23 | chunkedGetMultipleAccountInfos, 24 | deserializeAccount, 25 | getAssociatedTokenAccount, 26 | getOnchainTime, 27 | getOrCreateATAInstruction, 28 | getVaultPdas, 29 | unwrapSOLInstruction, 30 | wrapSOLInstruction, 31 | } from './utils'; 32 | import { AFFILIATE_PROGRAM_ID, PROGRAM_ID, VAULT_STRATEGY_ADDRESS } from './constants'; 33 | import { StrategyState } from './strategy'; 34 | import { IDL, Vault as VaultIdl } from './idl'; 35 | import { IDL as AffiliateIDL, AffiliateVault as AffiliateVaultIdl } from './affiliate-idl'; 36 | import { calculateWithdrawableAmount } from './helper'; 37 | import VaultHandler from './strategy/vault'; 38 | 39 | type VaultDetails = { 40 | vaultMint: Mint; 41 | vaultLpMint: Mint; 42 | vaultPda: PublicKey; 43 | tokenVaultPda: PublicKey; 44 | vaultState: VaultState; 45 | }; 46 | 47 | type WithdrawOpt = { 48 | affiliate: { 49 | affiliateId: PublicKey; 50 | affiliateProgram: AffiliateVaultProgram; 51 | partner: PublicKey; 52 | user: PublicKey; 53 | }; 54 | }; 55 | 56 | const getAllVaultState = async ( 57 | tokensAddress: Array, 58 | program: VaultProgram, 59 | seedBaseKey?: PublicKey, 60 | ): Promise> => { 61 | const vaultAccountPdas = tokensAddress.map((tokenMint) => 62 | getVaultPdas(tokenMint, new PublicKey(program.programId), seedBaseKey), 63 | ); 64 | 65 | const vaultsPda = vaultAccountPdas.map(({ vaultPda }) => vaultPda); 66 | const vaultsState = (await chunkedFetchMultipleVaultAccount(program, vaultsPda)) as Array; 67 | 68 | if (vaultsState.length !== tokensAddress.length) { 69 | throw new Error('Some of the vault state cannot be fetched'); 70 | } 71 | 72 | const vaultLpMints = vaultsState.map((vaultState) => vaultState.lpMint); 73 | const vaultMints = vaultsState.map((vaultState) => vaultState.tokenMint); 74 | const vaultLpAccounts = await chunkedGetMultipleAccountInfos(program.provider.connection, [ 75 | ...vaultLpMints, 76 | ...vaultMints, 77 | ]); 78 | 79 | return vaultsState.map((vaultState, index) => { 80 | const vaultAccountPda = vaultAccountPdas[index]; 81 | if (!vaultAccountPda) throw new Error('Missing vault account pda'); 82 | const vaultLpAccount = vaultLpAccounts[index]; 83 | if (!vaultLpAccount) throw new Error('Missing vault lp account'); 84 | const vaultLpMint = unpackMint(vaultState.lpMint, vaultLpAccount, vaultLpAccount.owner); 85 | const vaultAccount = vaultLpAccounts[index + vaultLpMints.length]; 86 | if (!vaultAccount) throw new Error('Missing vault account'); 87 | const vaultMint = unpackMint(vaultState.tokenMint, vaultAccount, vaultAccount.owner); 88 | 89 | return { vaultPda: vaultAccountPda.vaultPda, vaultState, vaultMint, vaultLpMint }; 90 | }); 91 | }; 92 | 93 | const getAllVaultStateByPda = async ( 94 | vaultsPda: Array, 95 | program: VaultProgram, 96 | ): Promise> => { 97 | const vaultsState = (await chunkedFetchMultipleVaultAccount(program, vaultsPda)) as Array; 98 | 99 | if (vaultsState.length !== vaultsPda.length) { 100 | throw new Error('Some of the vault state cannot be fetched'); 101 | } 102 | 103 | const vaultLpMints = vaultsState.map((vaultState) => vaultState.lpMint); 104 | const vaultMints = vaultsState.map((vaultState) => vaultState.tokenMint); 105 | const vaultLpAccounts = await chunkedGetMultipleAccountInfos(program.provider.connection, [ 106 | ...vaultLpMints, 107 | ...vaultMints, 108 | ]); 109 | 110 | return vaultsState.map((vaultState, index) => { 111 | const vaultPda = vaultsPda[index]; 112 | if (!vaultPda) throw new Error('Missing vault account pda'); 113 | const vaultLpAccount = vaultLpAccounts[index]; 114 | if (!vaultLpAccount) throw new Error('Missing vault lp account'); 115 | const vaultLpMint = unpackMint(vaultState.lpMint, vaultLpAccount, vaultLpAccount.owner); 116 | const vaultAccount = vaultLpAccounts[index + vaultLpMints.length]; 117 | if (!vaultAccount) throw new Error('Missing vault account'); 118 | const vaultMint = unpackMint(vaultState.tokenMint, vaultAccount, vaultAccount.owner); 119 | 120 | return { 121 | vaultPda, 122 | vaultState, 123 | vaultLpMint, 124 | vaultMint, 125 | }; 126 | }); 127 | }; 128 | 129 | const getVaultState = async ( 130 | tokenAddress: PublicKey, 131 | program: VaultProgram, 132 | seedBaseKey?: PublicKey, 133 | ): Promise => { 134 | const { vaultPda } = getVaultPdas(tokenAddress, new PublicKey(program.programId), seedBaseKey); 135 | const vaultState = (await program.account.vault.fetchNullable(vaultPda)) as VaultState; 136 | 137 | if (!vaultState) { 138 | throw 'Cannot get vault state'; 139 | } 140 | 141 | const [vaultLpAccount, vaultAccount] = await chunkedGetMultipleAccountInfos(program.provider.connection, [ 142 | vaultState.lpMint, 143 | vaultState.tokenMint, 144 | ]); 145 | const vaultLpMint = unpackMint(vaultState.lpMint, vaultLpAccount, vaultLpAccount?.owner); 146 | const vaultMint = unpackMint(vaultState.tokenMint, vaultAccount, vaultAccount?.owner); 147 | 148 | return { 149 | vaultPda, 150 | vaultState, 151 | vaultLpMint, 152 | vaultMint, 153 | }; 154 | }; 155 | 156 | const getVaultStateByPda = async (vaultPda: PublicKey, program: VaultProgram): Promise => { 157 | const vaultState = (await program.account.vault.fetchNullable(vaultPda)) as VaultState; 158 | 159 | if (!vaultState) { 160 | throw 'Cannot get vault state'; 161 | } 162 | 163 | const [vaultLpAccount, vaultAccount] = await chunkedGetMultipleAccountInfos(program.provider.connection, [ 164 | vaultState.lpMint, 165 | vaultState.tokenMint, 166 | ]); 167 | const vaultLpMint = unpackMint(vaultState.lpMint, vaultLpAccount, vaultLpAccount?.owner); 168 | const vaultMint = unpackMint(vaultState.tokenMint, vaultAccount, vaultAccount?.owner); 169 | 170 | return { 171 | vaultPda, 172 | vaultState, 173 | vaultLpMint, 174 | vaultMint, 175 | }; 176 | }; 177 | 178 | const getVaultLiquidity = async (connection: Connection, tokenVaultPda: PublicKey): Promise => { 179 | const vaultLiquidityResponse = await connection.getAccountInfo(tokenVaultPda); 180 | if (!vaultLiquidityResponse) return null; 181 | 182 | const vaultLiquiditySerialize = deserializeAccount(vaultLiquidityResponse.data); 183 | return vaultLiquiditySerialize?.amount.toString() || null; 184 | }; 185 | 186 | export default class VaultImpl implements VaultImplementation { 187 | private connection: Connection; 188 | private cluster: Cluster = 'mainnet-beta'; 189 | 190 | // Vault 191 | private program: VaultProgram; 192 | private affiliateId: PublicKey | undefined; 193 | private affiliateProgram: AffiliateVaultProgram | undefined; 194 | 195 | private allowOwnerOffCurve?: boolean; 196 | public seedBaseKey?: PublicKey; 197 | 198 | public tokenMint: Mint; 199 | public tokenLpMint: Mint; 200 | public vaultPda: PublicKey; 201 | public tokenVaultPda: PublicKey; 202 | public vaultState: VaultState; 203 | 204 | private constructor( 205 | program: VaultProgram, 206 | vaultDetails: VaultDetails, 207 | opt?: { 208 | seedBaseKey?: PublicKey; 209 | allowOwnerOffCurve?: boolean; 210 | cluster?: Cluster; 211 | affiliateId?: PublicKey; 212 | affiliateProgram?: AffiliateVaultProgram; 213 | }, 214 | ) { 215 | this.connection = program.provider.connection; 216 | this.cluster = opt?.cluster ?? 'mainnet-beta'; 217 | 218 | this.program = program; 219 | this.affiliateProgram = opt?.affiliateProgram; 220 | this.affiliateId = opt?.affiliateId; 221 | 222 | this.allowOwnerOffCurve = opt?.allowOwnerOffCurve; 223 | 224 | this.tokenLpMint = vaultDetails.vaultLpMint; 225 | this.tokenMint = vaultDetails.vaultMint; 226 | this.vaultPda = vaultDetails.vaultPda; 227 | this.tokenVaultPda = vaultDetails.tokenVaultPda; 228 | this.vaultState = vaultDetails.vaultState; 229 | } 230 | 231 | public static async createPermissionlessVaultInstruction( 232 | connection: Connection, 233 | payer: PublicKey, 234 | tokenMint: PublicKey, 235 | opt?: { 236 | cluster?: Cluster; 237 | programId?: string; 238 | }, 239 | ) { 240 | const provider = new AnchorProvider(connection, {} as any, AnchorProvider.defaultOptions()); 241 | const program = new Program(IDL as VaultIdl, opt?.programId || PROGRAM_ID, provider); 242 | 243 | const { 244 | vaultPda: vault, 245 | tokenVaultPda: tokenVault, 246 | lpMintPda: lpMint, 247 | } = getVaultPdas(tokenMint, program.programId); 248 | 249 | return program.methods 250 | .initialize() 251 | .accounts({ 252 | vault, 253 | payer, 254 | tokenVault, 255 | tokenMint, 256 | lpMint, 257 | systemProgram: SystemProgram.programId, 258 | rent: SYSVAR_RENT_PUBKEY, 259 | tokenProgram: TOKEN_PROGRAM_ID, 260 | }) 261 | .instruction(); 262 | } 263 | 264 | public static async fetchMultipleUserBalance( 265 | connection: Connection, 266 | lpMintList: Array, 267 | owner: PublicKey, 268 | ): Promise> { 269 | const ataAccounts = lpMintList.map((lpMint) => getAssociatedTokenAccount(lpMint, owner)); 270 | 271 | const accountsInfo = await chunkedGetMultipleAccountInfos(connection, ataAccounts); 272 | 273 | return accountsInfo.map((accountInfo) => { 274 | if (!accountInfo) return new BN(0); 275 | 276 | const accountBalance = deserializeAccount(accountInfo.data); 277 | if (!accountBalance) throw new Error('Failed to parse user account for LP token.'); 278 | 279 | return new BN(accountBalance.amount.toString()); 280 | }); 281 | } 282 | 283 | public static async createMultiple( 284 | connection: Connection, 285 | tokenMints: Array, 286 | opt?: { 287 | seedBaseKey?: PublicKey; 288 | allowOwnerOffCurve?: boolean; 289 | cluster?: Cluster; 290 | programId?: string; 291 | affiliateId?: PublicKey; 292 | affiliateProgramId?: string; 293 | }, 294 | ): Promise> { 295 | const provider = new AnchorProvider(connection, {} as any, AnchorProvider.defaultOptions()); 296 | const program = new Program(IDL as VaultIdl, opt?.programId || PROGRAM_ID, provider); 297 | 298 | const vaultsStateInfo = await getAllVaultState(tokenMints, program); 299 | 300 | return Promise.all( 301 | vaultsStateInfo.map(async ({ vaultPda, vaultState, vaultLpMint, vaultMint }) => { 302 | return new VaultImpl( 303 | program, 304 | { 305 | vaultPda, 306 | tokenVaultPda: vaultState.tokenVault, 307 | vaultState, 308 | vaultLpMint, 309 | vaultMint, 310 | }, 311 | { 312 | ...opt, 313 | affiliateId: opt?.affiliateId, 314 | affiliateProgram: opt?.affiliateId 315 | ? new Program( 316 | AffiliateIDL as AffiliateVaultIdl, 317 | opt?.affiliateProgramId || AFFILIATE_PROGRAM_ID, 318 | provider, 319 | ) 320 | : undefined, 321 | }, 322 | ); 323 | }), 324 | ); 325 | } 326 | 327 | public static async createMultipleWithPda( 328 | connection: Connection, 329 | vaultsPda: Array, 330 | opt?: { 331 | seedBaseKey?: PublicKey; 332 | allowOwnerOffCurve?: boolean; 333 | cluster?: Cluster; 334 | programId?: string; 335 | affiliateId?: PublicKey; 336 | affiliateProgramId?: string; 337 | }, 338 | ): Promise> { 339 | const provider = new AnchorProvider(connection, {} as any, AnchorProvider.defaultOptions()); 340 | const program = new Program(IDL as VaultIdl, opt?.programId || PROGRAM_ID, provider); 341 | 342 | const vaultsStateInfo = await getAllVaultStateByPda(vaultsPda, program); 343 | 344 | return Promise.all( 345 | vaultsStateInfo.map(async ({ vaultPda, vaultState, vaultMint, vaultLpMint }) => { 346 | return new VaultImpl( 347 | program, 348 | { 349 | vaultPda, 350 | tokenVaultPda: vaultState.tokenVault, 351 | vaultState, 352 | vaultMint, 353 | vaultLpMint, 354 | }, 355 | { 356 | ...opt, 357 | affiliateId: opt?.affiliateId, 358 | affiliateProgram: opt?.affiliateId 359 | ? new Program( 360 | AffiliateIDL as AffiliateVaultIdl, 361 | opt?.affiliateProgramId || AFFILIATE_PROGRAM_ID, 362 | provider, 363 | ) 364 | : undefined, 365 | }, 366 | ); 367 | }), 368 | ); 369 | } 370 | 371 | public static async create( 372 | connection: Connection, 373 | tokenAddress: PublicKey, 374 | opt?: { 375 | seedBaseKey?: PublicKey; 376 | allowOwnerOffCurve?: boolean; 377 | cluster?: Cluster; 378 | programId?: string; 379 | affiliateId?: PublicKey; 380 | affiliateProgramId?: string; 381 | }, 382 | ): Promise { 383 | const provider = new AnchorProvider(connection, {} as any, AnchorProvider.defaultOptions()); 384 | const program = new Program(IDL as VaultIdl, opt?.programId || PROGRAM_ID, provider); 385 | 386 | const { vaultPda, vaultState, vaultMint, vaultLpMint } = await getVaultState(tokenAddress, program); 387 | return new VaultImpl( 388 | program, 389 | { 390 | vaultMint, 391 | vaultLpMint, 392 | vaultPda, 393 | tokenVaultPda: vaultState.tokenVault, 394 | vaultState, 395 | }, 396 | { 397 | ...opt, 398 | affiliateId: opt?.affiliateId, 399 | affiliateProgram: opt?.affiliateId 400 | ? new Program( 401 | AffiliateIDL as AffiliateVaultIdl, 402 | opt?.affiliateProgramId || AFFILIATE_PROGRAM_ID, 403 | provider, 404 | ) 405 | : undefined, 406 | }, 407 | ); 408 | } 409 | 410 | public async getUserBalance(owner: PublicKey): Promise { 411 | const isAffiliated = this.affiliateId && this.affiliateProgram; 412 | 413 | const address = await (async () => { 414 | // User deposit directly 415 | if (!isAffiliated) { 416 | return getAssociatedTokenAccount(this.vaultState.lpMint, owner); 417 | } 418 | 419 | // Get user affiliated address with the partner 420 | const { userLpToken } = await this.createAffiliateATAPreInstructions(owner); 421 | return userLpToken; 422 | })(); 423 | const accountInfo = await this.connection.getAccountInfo(address); 424 | 425 | if (!accountInfo) { 426 | return new BN(0); 427 | } 428 | 429 | const result = deserializeAccount(accountInfo.data); 430 | if (result == undefined) { 431 | throw new Error('Failed to parse user account for LP token.'); 432 | } 433 | 434 | return new BN(result.amount.toString()); 435 | } 436 | 437 | /** To refetch the latest lpSupply */ 438 | /** Use vaultImpl.lpSupply to use cached result */ 439 | public async getVaultSupply(): Promise { 440 | const vaultLpMint = await getMint(this.connection, this.vaultState.lpMint); 441 | this.tokenLpMint = vaultLpMint; 442 | return new BN(vaultLpMint.supply.toString()); 443 | } 444 | 445 | public async getWithdrawableAmount(): Promise { 446 | const currentTime = await getOnchainTime(this.connection); 447 | 448 | return calculateWithdrawableAmount(currentTime, this.vaultState); 449 | } 450 | 451 | public async refreshVaultState() { 452 | const { vaultState, vaultMint } = await getVaultStateByPda(this.vaultPda, this.program); 453 | this.vaultState = vaultState; 454 | this.tokenMint = vaultMint; 455 | } 456 | 457 | private async createATAPreInstructions(owner: PublicKey) { 458 | let preInstructions: TransactionInstruction[] = []; 459 | const [userToken, createUserTokenIx] = await getOrCreateATAInstruction( 460 | this.tokenMint.address, 461 | owner, 462 | this.connection, 463 | ); 464 | const [userLpToken, createUserLpTokenIx] = await getOrCreateATAInstruction( 465 | this.vaultState.lpMint, 466 | owner, 467 | this.connection, 468 | ); 469 | if (createUserTokenIx) { 470 | preInstructions.push(createUserTokenIx); 471 | } 472 | if (createUserLpTokenIx) { 473 | preInstructions.push(createUserLpTokenIx); 474 | } 475 | 476 | return { 477 | preInstructions, 478 | userToken, 479 | userLpToken, 480 | }; 481 | } 482 | 483 | private async createAffiliateATAPreInstructions(owner: PublicKey) { 484 | if (!this.affiliateId || !this.affiliateProgram) throw new Error('Affiliate ID or program not found'); 485 | 486 | const partner = this.affiliateId; 487 | const partnerToken = getAssociatedTokenAccount(this.tokenMint.address, partner); 488 | 489 | const [partnerAddress, _nonce] = PublicKey.findProgramAddressSync( 490 | [this.vaultPda.toBuffer(), partnerToken.toBuffer()], 491 | this.affiliateProgram.programId, 492 | ); 493 | const [userAddress, _nonceUser] = PublicKey.findProgramAddressSync( 494 | [partnerAddress.toBuffer(), owner.toBuffer()], 495 | this.affiliateProgram.programId, 496 | ); 497 | 498 | let preInstructions: TransactionInstruction[] = []; 499 | const [userToken, createUserTokenIx] = await getOrCreateATAInstruction( 500 | this.tokenMint.address, 501 | owner, 502 | this.connection, 503 | ); 504 | const [userLpToken, createUserLpTokenIx] = await getOrCreateATAInstruction( 505 | this.vaultState.lpMint, 506 | userAddress, 507 | this.connection, 508 | { 509 | payer: owner, 510 | }, 511 | ); 512 | if (createUserTokenIx) { 513 | preInstructions.push(createUserTokenIx); 514 | } 515 | if (createUserLpTokenIx) { 516 | preInstructions.push(createUserLpTokenIx); 517 | } 518 | 519 | return { 520 | preInstructions, 521 | partner, 522 | partnerAddress, 523 | userAddress, 524 | userToken, 525 | userLpToken, 526 | }; 527 | } 528 | 529 | public async deposit(owner: PublicKey, baseTokenAmount: BN): Promise { 530 | // Refresh vault state 531 | await this.refreshVaultState(); 532 | 533 | let preInstructions: TransactionInstruction[] = []; 534 | 535 | let partnerAddress: PublicKey | undefined; 536 | let userAddress: PublicKey | undefined; 537 | let userToken: PublicKey | undefined; 538 | let userLpToken: PublicKey | undefined; 539 | 540 | // Withdraw with Affiliate 541 | if (this.affiliateId && this.affiliateProgram) { 542 | const { 543 | preInstructions: preInstructionsATA, 544 | partnerAddress: partnerAddressATA, 545 | userAddress: userAddressATA, 546 | userToken: userTokenATA, 547 | userLpToken: userLpTokenATA, 548 | } = await this.createAffiliateATAPreInstructions(owner); 549 | preInstructions = preInstructionsATA; 550 | userToken = userTokenATA; 551 | userLpToken = userLpTokenATA; 552 | partnerAddress = partnerAddressATA; 553 | userAddress = userAddressATA; 554 | } else { 555 | // Without affiliate 556 | const { 557 | preInstructions: preInstructionsATA, 558 | userToken: userTokenATA, 559 | userLpToken: userLpTokenATA, 560 | } = await this.createATAPreInstructions(owner); 561 | preInstructions = preInstructionsATA; 562 | userToken = userTokenATA; 563 | userLpToken = userLpTokenATA; 564 | } 565 | 566 | // If it's SOL vault, wrap desired amount of SOL 567 | if (this.tokenMint.address.equals(NATIVE_MINT)) { 568 | preInstructions = preInstructions.concat(wrapSOLInstruction(owner, userToken, baseTokenAmount)); 569 | } 570 | 571 | let depositTx: Transaction; 572 | if (partnerAddress && userAddress && this.affiliateId && this.affiliateProgram) { 573 | const userPda = await this.connection.getParsedAccountInfo(userAddress); 574 | if (!userPda || !userPda.value?.data) { 575 | // Init first time user 576 | preInstructions.push( 577 | await this.affiliateProgram.methods 578 | .initUser() 579 | .accounts({ 580 | user: userAddress, 581 | partner: partnerAddress, 582 | owner, 583 | systemProgram: SystemProgram.programId, 584 | rent: SYSVAR_RENT_PUBKEY, 585 | }) 586 | .instruction(), 587 | ); 588 | } 589 | 590 | depositTx = await this.affiliateProgram.methods 591 | .deposit(new BN(baseTokenAmount.toString()), new BN(0)) // Vault does not have slippage, second parameter is ignored. 592 | .accounts({ 593 | partner: partnerAddress, 594 | user: userAddress, 595 | vaultProgram: this.program.programId, 596 | vault: this.vaultPda, 597 | tokenVault: this.tokenVaultPda, 598 | vaultLpMint: this.vaultState.lpMint, 599 | userToken, 600 | userLp: userLpToken, 601 | owner, 602 | tokenProgram: TOKEN_PROGRAM_ID, 603 | }) 604 | .preInstructions(preInstructions) 605 | .transaction(); 606 | } else { 607 | depositTx = await this.program.methods 608 | .deposit(new BN(baseTokenAmount.toString()), new BN(0)) // Vault does not have slippage, second parameter is ignored. 609 | .accounts({ 610 | vault: this.vaultPda, 611 | tokenVault: this.tokenVaultPda, 612 | lpMint: this.vaultState.lpMint, 613 | userToken, 614 | userLp: userLpToken, 615 | user: owner, 616 | tokenProgram: TOKEN_PROGRAM_ID, 617 | }) 618 | .preInstructions(preInstructions) 619 | .transaction(); 620 | } 621 | return new Transaction({ feePayer: owner, ...(await this.connection.getLatestBlockhash()) }).add(depositTx); 622 | } 623 | 624 | public async getStrategiesState(): Promise> { 625 | return ( 626 | await this.program.account.strategy.fetchMultiple( 627 | this.vaultState.strategies.filter((address) => address.toString() !== VAULT_STRATEGY_ADDRESS), 628 | ) 629 | ).filter(Boolean); 630 | } 631 | 632 | private async getStrategyWithHighestLiquidity(strategy?: PublicKey) { 633 | // Reserved for testing 634 | if (strategy) { 635 | const strategyState = (await this.program.account.strategy.fetchNullable(strategy)) as unknown as StrategyState; 636 | return { publicKey: strategy, strategyState }; 637 | } 638 | 639 | const vaultStrategiesStatePromise = this.vaultState.strategies 640 | .filter((address) => address.toString() !== VAULT_STRATEGY_ADDRESS) 641 | .map(async (strat) => { 642 | const strategyState = (await this.program.account.strategy.fetch(strat)) as unknown as StrategyState; 643 | return { publicKey: strat, strategyState }; 644 | }); 645 | const vaultStrategiesState = await Promise.allSettled(vaultStrategiesStatePromise); 646 | const settledVaultStrategiesState = vaultStrategiesState 647 | .map((item) => (item.status === 'fulfilled' ? item.value : undefined)) 648 | .filter(Boolean); 649 | 650 | const highestLiquidity = settledVaultStrategiesState.sort((a, b) => 651 | b.strategyState.currentLiquidity.sub(a.strategyState.currentLiquidity).toNumber(), 652 | )[0]; 653 | return highestLiquidity; 654 | } 655 | 656 | public async withdraw(owner: PublicKey, baseTokenAmount: BN): Promise { 657 | // Refresh vault state 658 | await this.refreshVaultState(); 659 | const lpSupply = await this.getVaultSupply(); 660 | 661 | let preInstructions: TransactionInstruction[] = []; 662 | let userToken: PublicKey | undefined; 663 | let userLpToken: PublicKey | undefined; 664 | let withdrawOpt: WithdrawOpt | undefined; 665 | 666 | // Withdraw with Affiliate 667 | if (this.affiliateId && this.affiliateProgram) { 668 | const { 669 | preInstructions: preInstructionsATA, 670 | partnerAddress, 671 | userAddress, 672 | userToken: userTokenATA, 673 | userLpToken: userLpTokenATA, 674 | } = await this.createAffiliateATAPreInstructions(owner); 675 | 676 | preInstructions = preInstructionsATA; 677 | userToken = userTokenATA; 678 | userLpToken = userLpTokenATA; 679 | withdrawOpt = 680 | this.affiliateId && this.affiliateProgram 681 | ? { 682 | affiliate: { 683 | affiliateId: this.affiliateId, 684 | affiliateProgram: this.affiliateProgram, 685 | partner: partnerAddress, 686 | user: userAddress, 687 | }, 688 | } 689 | : undefined; 690 | } else { 691 | // Without affiliate 692 | const { 693 | preInstructions: preInstructionsATA, 694 | userToken: userTokenATA, 695 | userLpToken: userLpTokenATA, 696 | } = await this.createATAPreInstructions(owner); 697 | preInstructions = preInstructionsATA; 698 | userToken = userTokenATA; 699 | userLpToken = userLpTokenATA; 700 | } 701 | 702 | const unlockedAmount = await this.getWithdrawableAmount(); 703 | const amountToWithdraw = baseTokenAmount.mul(unlockedAmount).div(lpSupply); 704 | const vaultLiquidity = new BN((await getVaultLiquidity(this.connection, this.tokenVaultPda)) || 0); 705 | 706 | if ( 707 | amountToWithdraw.lt(vaultLiquidity) // If withdraw amount lesser than vault reserve 708 | ) { 709 | return this.withdrawFromVaultReserve( 710 | owner, 711 | baseTokenAmount, 712 | userToken, 713 | userLpToken, 714 | preInstructions, 715 | withdrawOpt, 716 | ); 717 | } 718 | 719 | // Get strategy with highest liquidity 720 | // opt.strategy reserved for testing 721 | const selectedStrategy = await this.getStrategyWithHighestLiquidity(); 722 | 723 | const currentLiquidity = new BN(selectedStrategy.strategyState.currentLiquidity); 724 | const availableAmount = currentLiquidity.add(vaultLiquidity); 725 | 726 | if (amountToWithdraw.gt(availableAmount)) { 727 | throw new Error('Selected strategy does not have enough liquidity.'); 728 | } 729 | 730 | const strategyHandler = new VaultHandler(); 731 | 732 | // Unwrap SOL 733 | const postInstruction: Array = []; 734 | if (this.tokenMint.address.equals(NATIVE_MINT)) { 735 | const closeWrappedSOLIx = await unwrapSOLInstruction(owner); 736 | if (closeWrappedSOLIx) { 737 | postInstruction.push(closeWrappedSOLIx); 738 | } 739 | } 740 | 741 | const withdrawFromStrategyTx = await strategyHandler.withdraw( 742 | owner, 743 | this.program, 744 | { 745 | pubkey: selectedStrategy.publicKey, 746 | state: selectedStrategy.strategyState, 747 | }, 748 | this.vaultPda, 749 | this.tokenVaultPda, 750 | this.vaultState, 751 | userToken, 752 | userLpToken, 753 | baseTokenAmount, 754 | preInstructions, 755 | postInstruction, 756 | withdrawOpt, 757 | ); 758 | 759 | const tx = new Transaction({ feePayer: owner, ...(await this.connection.getLatestBlockhash()) }).add( 760 | withdrawFromStrategyTx, 761 | ); 762 | 763 | return tx; 764 | } 765 | 766 | // Reserved code to withdraw from Vault Reserves directly. 767 | // The only situation this piece of code will be required, is when a single Vault have no other strategy, and only have its own reserve. 768 | private async withdrawFromVaultReserve( 769 | owner: PublicKey, 770 | baseTokenAmount: BN, 771 | userToken: PublicKey, 772 | userLpToken: PublicKey, 773 | preInstructions: Array, 774 | withdrawOpt?: WithdrawOpt, 775 | ): Promise { 776 | // Unwrap SOL 777 | const postInstruction: Array = []; 778 | if (this.tokenMint.address.equals(NATIVE_MINT)) { 779 | const closeWrappedSOLIx = await unwrapSOLInstruction(owner); 780 | if (closeWrappedSOLIx) { 781 | postInstruction.push(closeWrappedSOLIx); 782 | } 783 | } 784 | 785 | let withdrawTx; 786 | if (withdrawOpt?.affiliate) { 787 | withdrawTx = await withdrawOpt.affiliate.affiliateProgram.methods 788 | .withdraw(baseTokenAmount, new BN(0)) 789 | .accounts({ 790 | vault: this.vaultPda, 791 | tokenVault: this.tokenVaultPda, 792 | vaultLpMint: this.vaultState.lpMint, 793 | partner: withdrawOpt.affiliate.partner, 794 | owner, 795 | userToken, 796 | vaultProgram: this.program.programId, 797 | userLp: userLpToken, 798 | user: withdrawOpt.affiliate.user, 799 | tokenProgram: TOKEN_PROGRAM_ID, 800 | }) 801 | .preInstructions(preInstructions) 802 | .postInstructions(postInstruction) 803 | .transaction(); 804 | } else { 805 | withdrawTx = await this.program.methods 806 | .withdraw(baseTokenAmount, new BN(0)) // Vault does not have slippage, second parameter is ignored. 807 | .accounts({ 808 | vault: this.vaultPda, 809 | tokenVault: this.tokenVaultPda, 810 | lpMint: this.vaultState.lpMint, 811 | userToken, 812 | userLp: userLpToken, 813 | user: owner, 814 | tokenProgram: TOKEN_PROGRAM_ID, 815 | }) 816 | .preInstructions(preInstructions) 817 | .postInstructions(postInstruction) 818 | .transaction(); 819 | } 820 | 821 | return new Transaction({ feePayer: owner, ...(await this.connection.getLatestBlockhash()) }).add(withdrawTx); 822 | } 823 | 824 | public async getAffiliateInfo(): Promise { 825 | if (!this.affiliateId || !this.affiliateProgram) throw new Error('No affiliateId or affiliate program found'); 826 | 827 | const partner = this.affiliateId; 828 | const partnerToken = getAssociatedTokenAccount(this.tokenMint.address, partner); 829 | 830 | const [partnerAddress, _nonce] = PublicKey.findProgramAddressSync( 831 | [this.vaultPda.toBuffer(), partnerToken.toBuffer()], 832 | this.affiliateProgram.programId, 833 | ); 834 | 835 | const partnerDetails = (await this.affiliateProgram.account.partner.fetchNullable(partnerAddress)) as AffiliateInfo; 836 | return partnerDetails; 837 | } 838 | } 839 | -------------------------------------------------------------------------------- /ts-client/src/vault/strategy/index.ts: -------------------------------------------------------------------------------- 1 | import { BN } from '@coral-xyz/anchor'; 2 | import { PublicKey, Transaction, TransactionInstruction } from '@solana/web3.js'; 3 | import type { AffiliateVaultProgram, VaultProgram, VaultState } from '../types'; 4 | 5 | export type StrategyType = 6 | | 'portFinanceWithoutLm' 7 | | 'portFinanceWithLm' 8 | | 'solendWithoutLm' 9 | | 'solendWithLm' 10 | | 'francium' 11 | | 'apricotWithoutLM' 12 | | 'mango' 13 | | 'tulip' 14 | | 'vault' 15 | | 'drift' 16 | | 'frakt' 17 | | 'cypher' 18 | | 'psylend' 19 | | 'marginfi'; 20 | 21 | export type StrategyState = { 22 | reserve: PublicKey; 23 | collateralVault: PublicKey; 24 | strategyType: object; 25 | bumps: number[]; 26 | currentLiquidity: BN; 27 | vault: PublicKey; 28 | }; 29 | 30 | export type Strategy = { 31 | pubkey: PublicKey; 32 | state: StrategyState; 33 | }; 34 | 35 | export type ReserveState = { 36 | collateral: { 37 | mintPubkey: PublicKey; 38 | mintTotalSupply: number; 39 | supplyPubkey: String; 40 | }; 41 | state: unknown; 42 | }; 43 | 44 | export interface StrategyHandler { 45 | strategyProgram?: PublicKey; 46 | withdraw( 47 | walletPubKey: PublicKey, 48 | program: VaultProgram, 49 | strategy: any, 50 | vault: PublicKey, 51 | tokenVault: PublicKey, 52 | vaultState: VaultState, 53 | userToken: PublicKey, 54 | userLp: PublicKey, 55 | amount: BN, 56 | preInstructions: TransactionInstruction[], 57 | postInstructions: TransactionInstruction[], 58 | opt?: { 59 | affiliate?: { 60 | affiliateId: PublicKey; 61 | affiliateProgram: AffiliateVaultProgram; 62 | partner: PublicKey; 63 | user: PublicKey; 64 | }; 65 | }, 66 | ): Promise; 67 | } 68 | 69 | export const getStrategyType = (strategyResponse: any) => { 70 | return Object.keys(strategyResponse)[0] as StrategyType; 71 | }; 72 | -------------------------------------------------------------------------------- /ts-client/src/vault/strategy/vault.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, TransactionInstruction } from '@solana/web3.js'; 2 | import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; 3 | import * as anchor from '@coral-xyz/anchor'; 4 | 5 | import { StrategyHandler, Strategy } from '.'; 6 | import { AffiliateVaultProgram, VaultProgram, VaultState } from '../types'; 7 | 8 | export default class VaultHandler implements StrategyHandler { 9 | async withdraw( 10 | walletPubKey: PublicKey, 11 | program: VaultProgram, 12 | _strategy: Strategy, 13 | vault: PublicKey, 14 | tokenVault: PublicKey, 15 | vaultState: VaultState, 16 | userToken: PublicKey, 17 | userLp: PublicKey, 18 | amount: anchor.BN, 19 | preInstructions: TransactionInstruction[], 20 | postInstructions: TransactionInstruction[], 21 | opt?: { 22 | affiliate?: { 23 | affiliateId: PublicKey; 24 | affiliateProgram: AffiliateVaultProgram; 25 | partner: PublicKey; 26 | user: PublicKey; 27 | }; 28 | }, 29 | ) { 30 | const txAccounts = { 31 | vault, 32 | tokenVault, 33 | userToken, 34 | userLp, 35 | tokenProgram: TOKEN_PROGRAM_ID, 36 | }; 37 | 38 | if (opt?.affiliate) { 39 | const tx = await opt.affiliate.affiliateProgram.methods 40 | .withdraw(amount, new anchor.BN(0)) 41 | .accounts({ 42 | ...txAccounts, 43 | partner: opt.affiliate.partner, 44 | user: opt.affiliate.user, 45 | vaultProgram: program.programId, 46 | vaultLpMint: vaultState.lpMint, 47 | owner: walletPubKey, 48 | }) 49 | .preInstructions(preInstructions) 50 | .postInstructions(postInstructions) 51 | .transaction(); 52 | 53 | return tx; 54 | } 55 | 56 | const tx = await program.methods 57 | .withdraw(amount, new anchor.BN(0)) 58 | .accounts({ 59 | ...txAccounts, 60 | lpMint: vaultState.lpMint, 61 | user: walletPubKey, 62 | }) 63 | .preInstructions(preInstructions) 64 | .postInstructions(postInstructions) 65 | .transaction(); 66 | 67 | return tx; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /ts-client/src/vault/tests/affiliate.test.ts: -------------------------------------------------------------------------------- 1 | import { Connection, Keypair, PublicKey } from '@solana/web3.js'; 2 | import { Wallet, AnchorProvider, BN } from '@coral-xyz/anchor'; 3 | 4 | import VaultImpl from '..'; 5 | import { airDropSol } from './utils'; 6 | import { NATIVE_MINT } from '@solana/spl-token'; 7 | import fs from "fs"; 8 | import os from 'os' 9 | 10 | let mockWallet = new Wallet(new Keypair()); 11 | 12 | // devnet ATA creation and reading must use confirmed. 13 | const devnetConnection = new Connection('https://api.devnet.solana.com/', { commitment: 'confirmed' }); 14 | 15 | 16 | // TODO: Remove this fake partner ID 17 | const TEMPORARY_PARTNER_PUBLIC_KEY = new PublicKey('7236FoaWTXJyzbfFPZcrzg3tBpPhGiTgXsGWvjwrYfiF'); 18 | describe('Interact with Vault in devnet', () => { 19 | const provider = new AnchorProvider(devnetConnection, mockWallet, { 20 | commitment: 'confirmed', 21 | }); 22 | 23 | let vaultImpl: VaultImpl; 24 | beforeAll(async () => { 25 | await airDropSol(devnetConnection, mockWallet.publicKey); 26 | vaultImpl = await VaultImpl.create(devnetConnection, NATIVE_MINT, { 27 | cluster: 'devnet', 28 | affiliateId: TEMPORARY_PARTNER_PUBLIC_KEY, 29 | }); 30 | }); 31 | 32 | test('Test affiliate init user, check balance, deposits, then withdraw all', async () => { 33 | // First deposit 34 | const depositTx = await vaultImpl.deposit(mockWallet.publicKey, new BN(100_000_000)); 35 | const depositResult = await provider.sendAndConfirm(depositTx); 36 | console.log('Deposit result', depositResult); 37 | expect(typeof depositResult).toBe('string'); 38 | 39 | // Check balance 40 | const userBalanceDeposit = await vaultImpl.getUserBalance(mockWallet.publicKey); 41 | expect(Number(userBalanceDeposit)).toBeGreaterThan(0); 42 | 43 | // Subsequent deposit should not create ATA, and no need to init user 44 | const depositTx2 = await vaultImpl.deposit(mockWallet.publicKey, new BN(100_000_000)); 45 | expect(depositTx2.instructions.map((ix) => ix.programId.toString())).toEqual([ 46 | '11111111111111111111111111111111', // Wrap SOL 47 | 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', // Sync nativve 48 | 'GacY9YuN16HNRTy7ZWwULPccwvfFSBeNLuAQP7y38Du3', // Affiliate program deposit 49 | ]); 50 | const depositResult2 = await provider.sendAndConfirm(depositTx2); 51 | console.log('Deposit result', depositResult2); 52 | expect(typeof depositResult2).toBe('string'); 53 | 54 | // Check balance again, should be greater than first deposit 55 | const userBalanceDeposit2 = await vaultImpl.getUserBalance(mockWallet.publicKey); 56 | expect(Number(userBalanceDeposit2)).toBeGreaterThan(Number(userBalanceDeposit)); 57 | 58 | // Withdraw 59 | const withdrawTx = await vaultImpl.withdraw(mockWallet.publicKey, userBalanceDeposit2); 60 | const withdrawResult = await provider.sendAndConfirm(withdrawTx); 61 | console.log('Withdraw result', withdrawResult); 62 | expect(typeof withdrawResult).toBe('string'); 63 | 64 | // Check final balance to be zero 65 | const userBalanceDeposit3 = await vaultImpl.getUserBalance(mockWallet.publicKey); 66 | expect(Number(userBalanceDeposit3)).toEqual(0); 67 | }); 68 | 69 | test('Get affiliate partner info', async () => { 70 | const partnerInfo = await vaultImpl.getAffiliateInfo(); 71 | expect(Object.keys(partnerInfo)).toEqual( 72 | expect.arrayContaining(['partnerToken', 'vault', 'outstandingFee', 'feeRatio', 'cummulativeFee']), 73 | ); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /ts-client/src/vault/tests/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey } from '@solana/web3.js'; 2 | 3 | const LAMPORTS_PER_SOL = 1e9; 4 | export const airDropSol = async (connection: Connection, publicKey: PublicKey, amount = 1 * LAMPORTS_PER_SOL) => { 5 | try { 6 | const airdropSignature = await connection.requestAirdrop(publicKey, amount); 7 | const latestBlockHash = await connection.getLatestBlockhash(); 8 | await connection.confirmTransaction({ 9 | blockhash: latestBlockHash.blockhash, 10 | lastValidBlockHeight: latestBlockHash.lastValidBlockHeight, 11 | signature: airdropSignature, 12 | }); 13 | } catch (error) { 14 | console.error(error); 15 | throw error; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /ts-client/src/vault/tests/vault.test.ts: -------------------------------------------------------------------------------- 1 | import { Connection, Keypair, PublicKey } from '@solana/web3.js'; 2 | import { Wallet, AnchorProvider, BN } from '@coral-xyz/anchor'; 3 | 4 | import VaultImpl from '..'; 5 | import { airDropSol } from './utils'; 6 | import { getVaultPdas } from '../utils'; 7 | import { PROGRAM_ID, USDC_MINT, USDT_MINT } from '../constants'; 8 | import { NATIVE_MINT } from '@solana/spl-token'; 9 | 10 | const mockWallet = new Wallet(new Keypair()); 11 | const mainnetConnection = new Connection('https://api.mainnet-beta.solana.com'); 12 | // devnet ATA creation and reading must use confirmed. 13 | const devnetConnection = new Connection('https://api.devnet.solana.com/', { commitment: 'confirmed' }); 14 | 15 | describe('Get Mainnet vault state', () => { 16 | let vaults: VaultImpl[] = []; 17 | let vaultsForPool: VaultImpl[] = []; 18 | 19 | // Make sure all vaults can be initialized 20 | beforeAll(async () => { 21 | const tokenAddresses = [NATIVE_MINT, USDC_MINT, USDT_MINT]; 22 | const tokensInfoPda = tokenAddresses.map((tokenAddress) => { 23 | const { vaultPda } = getVaultPdas(tokenAddress, new PublicKey(PROGRAM_ID)); 24 | return vaultPda; 25 | }); 26 | vaults = await VaultImpl.createMultiple(mainnetConnection, tokenAddresses); 27 | vaultsForPool = await VaultImpl.createMultipleWithPda(mainnetConnection, tokensInfoPda); 28 | }); 29 | 30 | test('Get LP Supply', async () => { 31 | const vaultLpSupplies = await Promise.all( 32 | vaults.map(async (vault) => { 33 | const lpSupply = await vault.getVaultSupply(); 34 | 35 | return lpSupply; 36 | }), 37 | ); 38 | 39 | vaultLpSupplies.forEach((lpSupply) => { 40 | expect(lpSupply.gtn(0)).toBeTruthy(); 41 | }); 42 | 43 | const vaultLpSuppliesForPool = await Promise.all( 44 | vaultsForPool.map(async (vault) => { 45 | const lpSupply = await vault.getVaultSupply(); 46 | 47 | return lpSupply; 48 | }), 49 | ); 50 | 51 | vaultLpSuppliesForPool.forEach((lpSupply) => { 52 | expect(lpSupply.gtn(0)).toBeTruthy(); 53 | }); 54 | }); 55 | 56 | test('Get unlocked amount', async () => { 57 | vaults.forEach(async (vault) => { 58 | const unlockedAmount = await vault.getWithdrawableAmount(); 59 | expect(Number(unlockedAmount)).toBeGreaterThan(0); 60 | }); 61 | }); 62 | }); 63 | 64 | describe('Interact with Vault in devnet', () => { 65 | const provider = new AnchorProvider(devnetConnection, mockWallet, { 66 | commitment: 'confirmed', 67 | }); 68 | 69 | let vault: VaultImpl; 70 | beforeAll(async () => { 71 | await airDropSol(devnetConnection, mockWallet.publicKey); 72 | vault = await VaultImpl.create(devnetConnection, NATIVE_MINT, { cluster: 'devnet' }); 73 | }); 74 | 75 | test('Deposit, check balance, withdraw', async () => { 76 | // Deposit 77 | const depositTx = await vault.deposit(mockWallet.publicKey, new BN(100_000_000)); 78 | const depositResult = await provider.sendAndConfirm(depositTx); 79 | console.log('Deposit result', depositResult); 80 | expect(typeof depositResult).toBe('string'); 81 | 82 | // Check balance 83 | const userBalanceDeposit = await vault.getUserBalance(mockWallet.publicKey); 84 | expect(Number(userBalanceDeposit)).toBeGreaterThan(0); 85 | 86 | // Withdraw all lp 87 | const withdrawTx = await vault.withdraw(mockWallet.publicKey, userBalanceDeposit); 88 | const withdrawResult = await provider.sendAndConfirm(withdrawTx); 89 | console.log('Withdraw result', withdrawResult); 90 | expect(typeof withdrawResult).toBe('string'); 91 | 92 | // Check balance 93 | const userBalanceWithdraw = await vault.getUserBalance(mockWallet.publicKey); 94 | expect(Number(userBalanceWithdraw)).toEqual(0); 95 | }); 96 | 97 | test('Vault Withdraw SOL from all strategy', async () => { 98 | for (var strategy of vault.vaultState.strategies) { 99 | if (!strategy.equals(PublicKey.default)) { 100 | console.log('Test with ', strategy.toString()); 101 | 102 | // Deposit 103 | const depositTx = await vault.deposit(mockWallet.publicKey, new BN(1_000_000)); 104 | const depositResult = await provider.sendAndConfirm(depositTx); 105 | expect(typeof depositResult).toBe('string'); 106 | 107 | // Withdraw from specific strategy 108 | const withdrawTx = await vault.withdraw(mockWallet.publicKey, new BN(1000)); 109 | 110 | try { 111 | const withdrawResult = await provider.sendAndConfirm(withdrawTx); 112 | console.log('Strategy withdraw result', withdrawResult); 113 | expect(typeof withdrawResult).toBe('string'); 114 | } catch (error) { 115 | console.log('Error creating withdrawFromStrategy instruction', error); 116 | } 117 | } 118 | } 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /ts-client/src/vault/types/index.ts: -------------------------------------------------------------------------------- 1 | import { BN, IdlTypes, Program } from '@coral-xyz/anchor'; 2 | import { PublicKey, Transaction } from '@solana/web3.js'; 3 | import { TypeDef } from '@coral-xyz/anchor/dist/cjs/program/namespace/types'; 4 | 5 | import { Vault as VaultIdl } from '../idl'; 6 | import { AffiliateVault as AffiliateVaultIdl } from '../affiliate-idl'; 7 | import { Mint } from '@solana/spl-token'; 8 | 9 | export type VaultProgram = Program; 10 | export type AffiliateVaultProgram = Program; 11 | 12 | export type VaultImplementation = { 13 | getUserBalance: (owner: PublicKey) => Promise; 14 | getVaultSupply: () => Promise; 15 | getWithdrawableAmount: (ownerPublicKey: PublicKey) => Promise; 16 | deposit: (owner: PublicKey, baseTokenAmount: BN) => Promise; 17 | withdraw: (owner: PublicKey, baseTokenAmount: BN) => Promise; 18 | 19 | // Affiliate 20 | getAffiliateInfo: () => Promise; 21 | }; 22 | 23 | export type VaultState = TypeDef>; 24 | 25 | /** Affiliate */ 26 | export interface AffiliateInfo { 27 | partnerToken: PublicKey; 28 | vault: PublicKey; 29 | outstandingFee: BN; 30 | feeRatio: BN; 31 | cummulativeFee: BN; 32 | } 33 | 34 | /** Utils */ 35 | export interface ParsedClockState { 36 | info: { 37 | epoch: number; 38 | epochStartTimestamp: number; 39 | leaderScheduleEpoch: number; 40 | slot: number; 41 | unixTimestamp: number; 42 | }; 43 | type: string; 44 | program: string; 45 | space: number; 46 | } 47 | 48 | export interface VaultStateAndLp { 49 | vaultPda: PublicKey; 50 | vaultState: VaultState; 51 | vaultLpMint: Mint; 52 | vaultMint: Mint; 53 | } 54 | -------------------------------------------------------------------------------- /ts-client/src/vault/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ASSOCIATED_TOKEN_PROGRAM_ID, 3 | TOKEN_PROGRAM_ID, 4 | NATIVE_MINT, 5 | getAssociatedTokenAddressSync, 6 | createAssociatedTokenAccountInstruction, 7 | createCloseAccountInstruction, 8 | RawAccount, 9 | AccountLayout, 10 | MintLayout, 11 | RawMint, 12 | } from '@solana/spl-token'; 13 | import { 14 | Connection, 15 | ParsedAccountData, 16 | PublicKey, 17 | SystemProgram, 18 | SYSVAR_CLOCK_PUBKEY, 19 | TransactionInstruction, 20 | } from '@solana/web3.js'; 21 | import { BN } from '@coral-xyz/anchor'; 22 | 23 | import { SEEDS, VAULT_BASE_KEY } from '../constants'; 24 | import { ParsedClockState, VaultProgram } from '../types'; 25 | 26 | export const getAssociatedTokenAccount = (tokenMint: PublicKey, owner: PublicKey) => { 27 | return getAssociatedTokenAddressSync(tokenMint, owner, true, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID); 28 | }; 29 | 30 | export const deserializeAccount = (data: Buffer | undefined): RawAccount | undefined => { 31 | if (data == undefined || data.length == 0) { 32 | return undefined; 33 | } 34 | const accountInfo = AccountLayout.decode(data); 35 | return accountInfo; 36 | }; 37 | 38 | export const deserializeMint = (data: Buffer | undefined): RawMint | undefined => { 39 | if (data == undefined || data.length == 0) { 40 | return undefined; 41 | } 42 | const mintInfo = MintLayout.decode(data); 43 | return mintInfo; 44 | }; 45 | 46 | export const getOrCreateATAInstruction = async ( 47 | tokenAddress: PublicKey, 48 | owner: PublicKey, 49 | connection: Connection, 50 | opt?: { 51 | payer?: PublicKey; 52 | }, 53 | ): Promise<[PublicKey, TransactionInstruction?]> => { 54 | let toAccount; 55 | try { 56 | toAccount = getAssociatedTokenAddressSync(tokenAddress, owner, true, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID); 57 | const account = await connection.getAccountInfo(toAccount); 58 | if (!account) { 59 | const ix = createAssociatedTokenAccountInstruction( 60 | opt?.payer || owner, 61 | toAccount, 62 | owner, 63 | tokenAddress, 64 | TOKEN_PROGRAM_ID, 65 | ASSOCIATED_TOKEN_PROGRAM_ID, 66 | ); 67 | return [toAccount, ix]; 68 | } 69 | return [toAccount, undefined]; 70 | } catch (e) { 71 | /* handle error */ 72 | console.error('Error::getOrCreateATAInstruction', e); 73 | throw e; 74 | } 75 | }; 76 | 77 | export const getVaultPdas = (tokenMint: PublicKey, programId: PublicKey, seedBaseKey?: PublicKey) => { 78 | const [vault, _vaultBump] = PublicKey.findProgramAddressSync( 79 | [Buffer.from(SEEDS.VAULT_PREFIX), tokenMint.toBuffer(), (seedBaseKey ?? VAULT_BASE_KEY).toBuffer()], 80 | programId, 81 | ); 82 | 83 | const [tokenVault] = PublicKey.findProgramAddressSync( 84 | [Buffer.from(SEEDS.TOKEN_VAULT_PREFIX), vault.toBuffer()], 85 | programId, 86 | ); 87 | const [lpMint] = PublicKey.findProgramAddressSync([Buffer.from(SEEDS.LP_MINT_PREFIX), vault.toBuffer()], programId); 88 | 89 | return { 90 | vaultPda: vault, 91 | tokenVaultPda: tokenVault, 92 | lpMintPda: lpMint, 93 | }; 94 | }; 95 | 96 | export const wrapSOLInstruction = (from: PublicKey, to: PublicKey, amount: BN): TransactionInstruction[] => { 97 | return [ 98 | SystemProgram.transfer({ 99 | fromPubkey: from, 100 | toPubkey: to, 101 | lamports: amount.toNumber(), 102 | }), 103 | new TransactionInstruction({ 104 | keys: [ 105 | { 106 | pubkey: to, 107 | isSigner: false, 108 | isWritable: true, 109 | }, 110 | ], 111 | data: Buffer.from(new Uint8Array([17])), 112 | programId: TOKEN_PROGRAM_ID, 113 | }), 114 | ]; 115 | }; 116 | 117 | export const unwrapSOLInstruction = async (walletPublicKey: PublicKey) => { 118 | const wSolATAAccount = getAssociatedTokenAddressSync( 119 | NATIVE_MINT, 120 | walletPublicKey, 121 | true, 122 | TOKEN_PROGRAM_ID, 123 | ASSOCIATED_TOKEN_PROGRAM_ID, 124 | ); 125 | 126 | if (wSolATAAccount) { 127 | const closedWrappedSolInstruction = createCloseAccountInstruction( 128 | wSolATAAccount, 129 | walletPublicKey, 130 | walletPublicKey, 131 | [], 132 | ); 133 | return closedWrappedSolInstruction; 134 | } 135 | return null; 136 | }; 137 | 138 | export const getOnchainTime = async (connection: Connection) => { 139 | const parsedClock = await connection.getParsedAccountInfo(SYSVAR_CLOCK_PUBKEY); 140 | 141 | const parsedClockAccount = (parsedClock.value!.data as ParsedAccountData).parsed as ParsedClockState; 142 | 143 | const currentTime = parsedClockAccount.info.unixTimestamp; 144 | return currentTime; 145 | }; 146 | 147 | export const getLpSupply = async (connection: Connection, tokenMint: PublicKey): Promise => { 148 | const context = await connection.getTokenSupply(tokenMint); 149 | return new BN(context.value.amount); 150 | }; 151 | 152 | export function chunks(array: T[], size: number): T[][] { 153 | return Array.apply(0, new Array(Math.ceil(array.length / size))).map((_, index) => 154 | array.slice(index * size, (index + 1) * size), 155 | ); 156 | } 157 | 158 | export async function chunkedFetchMultipleVaultAccount( 159 | program: VaultProgram, 160 | pks: PublicKey[], 161 | chunkSize: number = 100, 162 | ) { 163 | const accounts = ( 164 | await Promise.all(chunks(pks, chunkSize).map((chunk) => program.account.vault.fetchMultiple(chunk))) 165 | ).flat(); 166 | 167 | return accounts.filter(Boolean); 168 | } 169 | 170 | export async function chunkedGetMultipleAccountInfos( 171 | connection: Connection, 172 | pks: PublicKey[], 173 | chunkSize: number = 100, 174 | ) { 175 | const accountInfos = ( 176 | await Promise.all(chunks(pks, chunkSize).map((chunk) => connection.getMultipleAccountsInfo(chunk))) 177 | ).flat(); 178 | 179 | return accountInfos; 180 | } 181 | -------------------------------------------------------------------------------- /ts-client/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/cjs/", 5 | "moduleResolution": "node", 6 | "noEmit": false, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "sourceMap": true, 10 | "esModuleInterop": true, 11 | "resolveJsonModule": true, 12 | "noImplicitAny": false, 13 | "skipLibCheck": true 14 | }, 15 | "exclude": ["**/*.test.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /ts-client/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "outDir": "dist/esm/", 5 | "target": "ES6", 6 | "module": "ESNext" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ts-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/recommended/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "types": ["jest", "mocha", "chai"], 7 | "typeRoots": ["./node_modules/@types"], 8 | "module": "commonjs", 9 | "target": "es6", 10 | "moduleResolution": "node", 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true, 13 | "noImplicitAny": false, 14 | "skipLibCheck": true 15 | } 16 | } 17 | --------------------------------------------------------------------------------