├── .github
└── CODEOWNERS
├── rust-toolchain.toml
├── Cargo.toml
├── programs
└── perpetuals
│ ├── Xargo.toml
│ ├── tests
│ └── native
│ │ ├── tests_suite
│ │ ├── swap
│ │ │ └── mod.rs
│ │ ├── liquidity
│ │ │ └── mod.rs
│ │ ├── mod.rs
│ │ └── position
│ │ │ └── mod.rs
│ │ ├── utils
│ │ ├── mod.rs
│ │ ├── pda.rs
│ │ └── fixtures.rs
│ │ ├── main.rs
│ │ └── instructions
│ │ ├── mod.rs
│ │ ├── test_set_custody_config.rs
│ │ ├── test_set_test_oracle_price.rs
│ │ ├── test_close_position.rs
│ │ ├── test_add_pool.rs
│ │ ├── test_init.rs
│ │ └── test_liquidate.rs
│ ├── src
│ ├── state.rs
│ ├── instructions
│ │ ├── get_assets_under_management.rs
│ │ ├── set_admin_signers.rs
│ │ ├── get_oracle_price.rs
│ │ ├── set_test_time.rs
│ │ ├── get_liquidation_state.rs
│ │ ├── remove_pool.rs
│ │ ├── set_permissions.rs
│ │ ├── get_pnl.rs
│ │ ├── set_test_oracle_price.rs
│ │ ├── get_exit_price_and_fee.rs
│ │ ├── withdraw_sol_fees.rs
│ │ ├── set_custody_config.rs
│ │ ├── get_entry_price_and_fee.rs
│ │ ├── test_init.rs
│ │ ├── get_remove_liquidity_amount_and_fee.rs
│ │ ├── init.rs
│ │ ├── get_add_liquidity_amount_and_fee.rs
│ │ ├── withdraw_fees.rs
│ │ └── get_liquidation_price.rs
│ ├── state
│ │ └── position.rs
│ ├── instructions.rs
│ └── error.rs
│ └── Cargo.toml
├── rustfmt.toml
├── .vscode
└── settings.json
├── .prettierignore
├── .gitignore
├── ui
├── postcss.config.js
├── next-env.d.ts
├── next.config.js
├── src
│ ├── styles
│ │ └── globals.css
│ ├── pages
│ │ ├── index.tsx
│ │ ├── trade
│ │ │ ├── index.tsx
│ │ │ └── [pair].tsx
│ │ ├── admin
│ │ │ └── index.tsx
│ │ ├── pools
│ │ │ └── [poolName].tsx
│ │ └── _app.tsx
│ ├── components
│ │ ├── Positions
│ │ │ ├── PositionColumn.tsx
│ │ │ ├── NoPositions.tsx
│ │ │ ├── PositionValueDelta.tsx
│ │ │ ├── index.tsx
│ │ │ ├── PoolPositionHeader.tsx
│ │ │ ├── ExistingPositions.tsx
│ │ │ └── PoolPositionRow.tsx
│ │ ├── Notify.tsx
│ │ ├── Layouts
│ │ │ ├── TradeLayout.tsx
│ │ │ └── PoolLayout.tsx
│ │ ├── Atoms
│ │ │ ├── MaxButton.tsx
│ │ │ ├── PoolBackButton.tsx
│ │ │ └── UserBalance.tsx
│ │ ├── PoolTokens.tsx
│ │ ├── SidebarTab.tsx
│ │ ├── Molecules
│ │ │ └── PoolHeaders
│ │ │ │ ├── TableHeader.tsx
│ │ │ │ └── TitleHeader.tsx
│ │ ├── LoadingDots.tsx
│ │ ├── NavbarLink.tsx
│ │ ├── Chart
│ │ │ ├── DailyStats.tsx
│ │ │ ├── CandlestickChart.tsx
│ │ │ └── ChartCurrency.tsx
│ │ ├── Icons
│ │ │ └── LoadingSpinner.tsx
│ │ ├── TradeSidebar
│ │ │ ├── TradeSwapDetails.tsx
│ │ │ ├── index.tsx
│ │ │ └── TradeDetails.tsx
│ │ ├── Navbar.tsx
│ │ ├── SolidButton.tsx
│ │ ├── PoolModal
│ │ │ ├── LpSelector.tsx
│ │ │ └── PoolGeneralStats.tsx
│ │ ├── TokenSelectorList.tsx
│ │ ├── LeverageSlider.tsx
│ │ ├── AirdropButton.tsx
│ │ └── PoolSelector.tsx
│ ├── lib
│ │ ├── UserAccount.tsx
│ │ ├── classGetters.ts
│ │ ├── PositionAccount.tsx
│ │ ├── CustodyAccount.tsx
│ │ └── types.tsx
│ ├── utils
│ │ ├── provider.ts
│ │ ├── organizers.ts
│ │ ├── formatters.ts
│ │ ├── retrieveData.ts
│ │ ├── constants.ts
│ │ └── transactionHelpers.ts
│ ├── hooks
│ │ ├── storeHelpers
│ │ │ ├── fetchCustodies.ts
│ │ │ ├── fetchPrices.ts
│ │ │ ├── fetchPositions.ts
│ │ │ ├── fetchUserData.ts
│ │ │ └── fetchPools.ts
│ │ └── useHydrateStore.ts
│ ├── stores
│ │ └── store.tsx
│ └── actions
│ │ └── closePosition.ts
├── tailwind.config.js
├── tsconfig.json
└── package.json
├── migrations
├── migrate-target.sh
└── deploy.ts
├── app
├── tsconfig.json
└── package.json
├── tsconfig.json
├── .githooks
└── pre-commit
├── Anchor.toml
├── LICENSE
├── package.json
└── DISCLAIMER.md
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @askibin
--------------------------------------------------------------------------------
/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | channel = "1.66.0"
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = [
3 | "programs/*"
4 | ]
5 |
--------------------------------------------------------------------------------
/programs/perpetuals/Xargo.toml:
--------------------------------------------------------------------------------
1 | [target.bpfel-unknown-unknown.dependencies.std]
2 | features = []
3 |
--------------------------------------------------------------------------------
/rustfmt.toml:
--------------------------------------------------------------------------------
1 | imports_granularity = "One"
2 | group_imports = "One"
3 | use_field_init_shorthand = true
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "rust-analyzer.rustfmt.extraArgs": [
3 | "+nightly"
4 | ],
5 | }
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 |
2 | .anchor
3 | .DS_Store
4 | target
5 | node_modules
6 | dist
7 | build
8 | test-ledger
9 |
--------------------------------------------------------------------------------
/programs/perpetuals/tests/native/tests_suite/swap/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod insuffisient_fund;
2 |
3 | pub use insuffisient_fund::*;
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .anchor
3 | .DS_Store
4 | target
5 | **/*.rs.bk
6 | **/node_modules
7 | test-ledger
8 |
9 | **/yarn.lock
10 |
11 | **/.next
12 |
--------------------------------------------------------------------------------
/ui/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | "postcss-import": {},
4 | tailwindcss: {},
5 | autoprefixer: {},
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/programs/perpetuals/tests/native/utils/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod fixtures;
2 | pub mod pda;
3 | #[allow(clippy::module_inception)]
4 | pub mod utils;
5 |
6 | pub use {fixtures::*, pda::*, utils::*};
7 |
--------------------------------------------------------------------------------
/programs/perpetuals/src/state.rs:
--------------------------------------------------------------------------------
1 | // Program state handling.
2 |
3 | pub mod custody;
4 | pub mod multisig;
5 | pub mod oracle;
6 | pub mod perpetuals;
7 | pub mod pool;
8 | pub mod position;
9 |
--------------------------------------------------------------------------------
/migrations/migrate-target.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | rm -rf ui/src/target/*
4 | mkdir ui/src/target/types
5 | mkdir ui/src/target/idl
6 | cp -rf target/idl ui/src/target
7 | cp -rf target/types ui/src/target
8 |
9 |
--------------------------------------------------------------------------------
/programs/perpetuals/tests/native/tests_suite/liquidity/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod fixed_fees;
2 | pub mod insuffisient_fund;
3 | pub mod min_max_ratio;
4 |
5 | pub use {fixed_fees::*, insuffisient_fund::*, min_max_ratio::*};
6 |
--------------------------------------------------------------------------------
/programs/perpetuals/tests/native/tests_suite/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod basic_interactions;
2 | pub mod liquidity;
3 | pub mod position;
4 | pub mod swap;
5 |
6 | pub use {basic_interactions::*, liquidity::*, position::*, swap::*};
7 |
--------------------------------------------------------------------------------
/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "typeRoots": ["./node_modules/@types"],
4 | "lib": ["es2015"],
5 | "module": "commonjs",
6 | "target": "es6",
7 | "esModuleInterop": true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/programs/perpetuals/tests/native/tests_suite/position/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod liquidate_position;
2 | pub mod max_user_profit;
3 | pub mod min_max_leverage;
4 |
5 | pub use {liquidate_position::*, max_user_profit::*, min_max_leverage::*};
6 |
--------------------------------------------------------------------------------
/ui/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "types": ["mocha", "chai"],
4 | "typeRoots": ["./node_modules/@types"],
5 | "lib": ["es2015"],
6 | "module": "commonjs",
7 | "target": "es6",
8 | "esModuleInterop": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.githooks/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | diff=$(cargo +nightly fmt -- --check)
4 | result=$?
5 |
6 | if [[ ${result} -ne 0 ]] ; then
7 | cat <<\EOF
8 | There are some code style issues, run `cargo +nightly fmt` first.
9 | EOF
10 | exit 1
11 | fi
12 |
13 | exit 0
--------------------------------------------------------------------------------
/ui/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | typescript: {
3 | // !! WARN !!
4 | // Dangerously allow production builds to successfully complete even if
5 | // your project has type errors.
6 | // !! WARN !!
7 | ignoreBuildErrors: true,
8 | },
9 | }
10 |
--------------------------------------------------------------------------------
/ui/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | input[type="number"]::-webkit-outer-spin-button,
6 | input[type="number"]::-webkit-inner-spin-button {
7 | -webkit-appearance: none;
8 | margin: 0;
9 | }
10 |
11 | input[type="number"] {
12 | -moz-appearance: textfield;
13 | }
14 |
--------------------------------------------------------------------------------
/ui/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import Router from "next/router";
3 |
4 | const IndexPage = () => {
5 | useEffect(() => {
6 | const { pathname } = Router;
7 | if (pathname == "/") {
8 | Router.push("/trade");
9 | }
10 | });
11 |
12 | return <>>;
13 | };
14 |
15 | export default IndexPage;
16 |
--------------------------------------------------------------------------------
/ui/src/pages/trade/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import Router from "next/router";
3 |
4 | const IndexPage = () => {
5 | useEffect(() => {
6 | const { pathname } = Router;
7 | if (pathname == "/trade") {
8 | Router.push("/trade/SOL-USD");
9 | }
10 | });
11 |
12 | return <>>;
13 | };
14 |
15 | export default IndexPage;
16 |
--------------------------------------------------------------------------------
/ui/src/components/Positions/PositionColumn.tsx:
--------------------------------------------------------------------------------
1 | export const COL_WIDTHS = {
2 | 1: 14,
3 | 2: 13,
4 | 3: 13,
5 | 4: 13,
6 | 5: 13,
7 | 6: 13,
8 | 7: 18,
9 | } as const;
10 |
11 | export function PositionColumn(props: {
12 | children: React.ReactNode;
13 | num: keyof typeof COL_WIDTHS;
14 | }) {
15 | return (
16 |
{props.children}
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/ui/src/components/Notify.tsx:
--------------------------------------------------------------------------------
1 | import { toast } from "react-toastify";
2 |
3 | // @ts-ignore
4 | export const notify = (
5 | text: string | JSX.Element,
6 | type = "success",
7 | hideProgressBar = false
8 | ) =>
9 | toast[type](text, {
10 | position: "bottom-left",
11 | autoClose: 3000,
12 | hideProgressBar: hideProgressBar,
13 | closeOnClick: true,
14 | pauseOnHover: true,
15 | draggable: true,
16 | progress: undefined,
17 | });
18 |
--------------------------------------------------------------------------------
/migrations/deploy.ts:
--------------------------------------------------------------------------------
1 | // Migrations are an early feature. Currently, they're nothing more than this
2 | // single deploy script that's invoked from the CLI, injecting a provider
3 | // configured from the workspace's Anchor.toml.
4 |
5 | const anchor = require("@project-serum/anchor");
6 |
7 | module.exports = async function (provider) {
8 | // Configure client to use the provider.
9 | anchor.setProvider(provider);
10 |
11 | // Add your deploy script here.
12 | };
13 |
--------------------------------------------------------------------------------
/Anchor.toml:
--------------------------------------------------------------------------------
1 | [features]
2 | seeds = false
3 | [programs.localnet]
4 | perpetuals = "Bmr31xzZYYVUdoHmAJL1DAp2anaitW8Tw9YfASS94MKJ"
5 | [programs.devnet]
6 | perpetuals = "Bmr31xzZYYVUdoHmAJL1DAp2anaitW8Tw9YfASS94MKJ"
7 |
8 | [registry]
9 | url = "https://anchor.projectserum.com"
10 |
11 | [provider]
12 | cluster = "localnet"
13 | wallet = "~/.config/solana/id.json"
14 |
15 | [scripts]
16 | test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 programs/perpetuals/tests/anchor/*.ts"
17 |
--------------------------------------------------------------------------------
/ui/src/components/Positions/NoPositions.tsx:
--------------------------------------------------------------------------------
1 | import ChartCandlestick from "@carbon/icons-react/lib/ChartCandlestick";
2 |
3 | interface Props {
4 | className?: string;
5 | emptyString?: string;
6 | }
7 |
8 | export function NoPositions(props: Props) {
9 | return (
10 |
11 |
12 |
{props.emptyString}
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/ui/src/lib/UserAccount.tsx:
--------------------------------------------------------------------------------
1 | import { TokenE } from "@/lib/Token";
2 |
3 | export class UserAccount {
4 | public lpBalances: Record;
5 | public tokenBalances: Record;
6 |
7 | constructor(
8 | lpBalances: Record = {},
9 | tokenBalances: Record = {}
10 | ) {
11 | this.lpBalances = lpBalances;
12 | this.tokenBalances = tokenBalances;
13 | }
14 |
15 | getUserLpBalance(poolAddress: string): number {
16 | return this.lpBalances[poolAddress] || 0;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 Solana Foundation.
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
--------------------------------------------------------------------------------
/programs/perpetuals/tests/native/main.rs:
--------------------------------------------------------------------------------
1 | pub mod instructions;
2 | pub mod tests_suite;
3 | pub mod utils;
4 |
5 | #[tokio::test]
6 | pub async fn test_integration() {
7 | tests_suite::basic_interactions().await;
8 |
9 | tests_suite::swap::insuffisient_fund().await;
10 |
11 | tests_suite::liquidity::fixed_fees().await;
12 | tests_suite::liquidity::insuffisient_fund().await;
13 | tests_suite::liquidity::min_max_ratio().await;
14 |
15 | tests_suite::position::min_max_leverage().await;
16 | tests_suite::position::liquidate_position().await;
17 | tests_suite::position::max_user_profit().await;
18 | }
19 |
--------------------------------------------------------------------------------
/ui/src/lib/classGetters.ts:
--------------------------------------------------------------------------------
1 | import { CustodyAccount } from "@/lib/CustodyAccount";
2 | import { PoolAccount } from "@/lib/PoolAccount";
3 | import { PriceStats } from "@/lib/types";
4 |
5 | export function getCurrentWeight(
6 | pool: PoolAccount,
7 | custody: CustodyAccount,
8 | stats: PriceStats
9 | ): number {
10 | let token = custody.getTokenE();
11 | const custodyAmount = Number(custody.assets.owned) / 10 ** custody.decimals;
12 |
13 | const custodyPrice = stats[token].currentPrice;
14 |
15 | return pool.getLiquidities(stats)
16 | ? (100 * custodyAmount * custodyPrice) / pool.getLiquidities(stats)
17 | : 0;
18 | }
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
4 | "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
5 | },
6 | "dependencies": {
7 | "@project-serum/anchor": "^0.25.0",
8 | "@solana/spl-token": "^0.3.6",
9 | "@solana/web3.js": "^1.70.1",
10 | "ts-node": "^10.9.1"
11 | },
12 | "devDependencies": {
13 | "@types/bn.js": "^5.1.1",
14 | "@types/chai": "^4.3.4",
15 | "@types/mocha": "^10.0.1",
16 | "chai": "^4.3.7",
17 | "mocha": "^10.2.0",
18 | "prettier": "^2.8.1",
19 | "ts-mocha": "^10.0.0",
20 | "typescript": "^4.9.4"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
4 | "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
5 | },
6 | "dependencies": {
7 | "@project-serum/anchor": "^0.25.0",
8 | "@solana/spl-token": "^0.3.6",
9 | "@solana/web3.js": "^1.70.1",
10 | "bs58": "^5.0.0",
11 | "commander": "^10.0.0",
12 | "fs": "^0.0.1-security",
13 | "js-sha256": "^0.9.0",
14 | "jsbi": "^4",
15 | "node-fetch": "^3.3.0",
16 | "ts-node": "^10.9.1"
17 | },
18 | "devDependencies": {
19 | "@types/bn.js": "^5.1.1",
20 | "prettier": "^2.8.1",
21 | "typescript": "^4.9.4"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/programs/perpetuals/tests/native/instructions/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod test_add_custody;
2 | pub mod test_add_liquidity;
3 | pub mod test_add_pool;
4 | pub mod test_close_position;
5 | pub mod test_init;
6 | pub mod test_liquidate;
7 | pub mod test_open_position;
8 | pub mod test_remove_liquidity;
9 | pub mod test_set_custody_config;
10 | pub mod test_set_test_oracle_price;
11 | pub mod test_swap;
12 |
13 | pub use {
14 | test_add_custody::*, test_add_liquidity::*, test_add_pool::*, test_close_position::*,
15 | test_init::*, test_liquidate::*, test_open_position::*, test_remove_liquidity::*,
16 | test_set_custody_config::*, test_set_test_oracle_price::*, test_swap::*,
17 | };
18 |
--------------------------------------------------------------------------------
/ui/src/components/Layouts/TradeLayout.tsx:
--------------------------------------------------------------------------------
1 | import { twMerge } from "tailwind-merge";
2 |
3 | interface Props {
4 | className?: string;
5 | children: [React.ReactNode, React.ReactNode];
6 | }
7 |
8 | export function TradeLayout(props: Props) {
9 | return (
10 |
23 |
{props.children[0]}
24 |
{props.children[1]}
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/ui/src/components/Atoms/MaxButton.tsx:
--------------------------------------------------------------------------------
1 | import { twMerge } from "tailwind-merge";
2 |
3 | interface Props {
4 | maxBalance?: number;
5 | onChangeAmount?: (amount: number) => void;
6 | }
7 | export function MaxButton(props: Props) {
8 | if (props.maxBalance && props.onChangeAmount) {
9 | return (
10 | props.onChangeAmount(props.maxBalance)}
21 | >
22 | Max
23 |
24 | );
25 | } else {
26 | return <>>;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/ui/src/utils/provider.ts:
--------------------------------------------------------------------------------
1 | import { Connection } from "@solana/web3.js";
2 | import { AnchorProvider, Wallet } from "@project-serum/anchor";
3 |
4 | export async function getProvider(
5 | wallet: Wallet,
6 | network: string = "devnet"
7 | ): Promise {
8 | let network_url;
9 | if (network === "devnet") {
10 | network_url = "https://api.devnet.solana.com";
11 | } else {
12 | network_url = "http://localhost:8899";
13 | }
14 |
15 | const connection = new Connection(network_url, {
16 | commitment: "processed",
17 | });
18 |
19 | const provider = new AnchorProvider(connection, wallet, {
20 | commitment: "processed",
21 | skipPreflight: true,
22 | });
23 | return provider;
24 | }
25 |
--------------------------------------------------------------------------------
/ui/src/components/Atoms/PoolBackButton.tsx:
--------------------------------------------------------------------------------
1 | import { ChevronLeft } from "@carbon/icons-react";
2 | import { useRouter } from "next/router";
3 | import { twMerge } from "tailwind-merge";
4 |
5 | interface Props {
6 | className?: string;
7 | }
8 | export default function PoolBackButton(props: Props) {
9 | const router = useRouter();
10 |
11 | return (
12 | router.push("/pools")}
21 | >
22 |
23 |
24 |
Back To Pools
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/ui/src/pages/admin/index.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingSpinner } from "@/components/Icons/LoadingSpinner";
2 | import { ExistingPositions } from "@/components/Positions/ExistingPositions";
3 | import { useGlobalStore } from "@/stores/store";
4 |
5 | interface Props {
6 | className?: string;
7 | }
8 |
9 | export default function Admin(props: Props) {
10 | const positionData = useGlobalStore((state) => state.positionData);
11 | return (
12 |
13 |
14 | All Positions
15 | {positionData.status === "pending" && (
16 |
17 | )}
18 |
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/ui/src/components/Layouts/PoolLayout.tsx:
--------------------------------------------------------------------------------
1 | import { twMerge } from "tailwind-merge";
2 |
3 | interface Props {
4 | className?: string;
5 | children: [React.ReactNode, React.ReactNode, React.ReactNode];
6 | }
7 |
8 | export function PoolLayout(props: Props) {
9 | return (
10 |
20 |
{props.children[0]}
21 |
30 |
{props.children[1]}
31 |
{props.children[2]}
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/ui/src/components/PoolTokens.tsx:
--------------------------------------------------------------------------------
1 | import { getTokenIcon, TokenE } from "@/lib/Token";
2 | import { cloneElement } from "react";
3 | import { twMerge } from "tailwind-merge";
4 |
5 | interface Props {
6 | className?: string;
7 | tokens: TokenE[];
8 | }
9 |
10 | export function PoolTokens(props: Props) {
11 | return (
12 |
13 | {props.tokens.slice(0, 3).map((token, i) => {
14 | const tokenIcon = getTokenIcon(token);
15 |
16 | return cloneElement(tokenIcon, {
17 | className: twMerge(
18 | tokenIcon.props.className,
19 | props.className,
20 | "border-black",
21 | "border",
22 | "rounded-full",
23 | "relative",
24 | "shrink-0"
25 | ),
26 | style: { zIndex: 3 - i },
27 | key: i,
28 | });
29 | })}
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/ui/src/hooks/storeHelpers/fetchCustodies.ts:
--------------------------------------------------------------------------------
1 | import { CustodyAccount } from "@/lib/CustodyAccount";
2 | import { Custody } from "@/lib/types";
3 | import { getPerpetualProgramAndProvider } from "@/utils/constants";
4 | import { PublicKey } from "@solana/web3.js";
5 |
6 | interface FetchCustody {
7 | account: Custody;
8 | publicKey: PublicKey;
9 | }
10 |
11 | export async function getCustodyData(): Promise<
12 | Record
13 | > {
14 | let { perpetual_program } = await getPerpetualProgramAndProvider();
15 |
16 | // @ts-ignore
17 | let fetchedCustodies: FetchCustody[] =
18 | await perpetual_program.account.custody.all();
19 |
20 | let custodyInfos: Record = fetchedCustodies.reduce(
21 | (acc: Record, { account, publicKey }) => (
22 | (acc[publicKey.toString()] = new CustodyAccount(account, publicKey)), acc
23 | ),
24 | {}
25 | );
26 |
27 | return custodyInfos;
28 | }
29 |
--------------------------------------------------------------------------------
/ui/src/components/SidebarTab.tsx:
--------------------------------------------------------------------------------
1 | import { twMerge } from "tailwind-merge";
2 |
3 | interface Props {
4 | className?: string;
5 | children: React.ReactNode;
6 | selected?: boolean;
7 | onClick?(): void;
8 | }
9 |
10 | export function SidebarTab(props: Props) {
11 | return (
12 |
35 | {props.children}
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/ui/src/components/Molecules/PoolHeaders/TableHeader.tsx:
--------------------------------------------------------------------------------
1 | import { PoolTokens } from "@/components/PoolTokens";
2 | import { PoolAccount } from "@/lib/PoolAccount";
3 | import { twMerge } from "tailwind-merge";
4 |
5 | interface Props {
6 | iconClassName?: string;
7 | poolClassName?: string;
8 | pool: PoolAccount;
9 | }
10 |
11 | export function TableHeader(props: Props) {
12 | return (
13 |
14 | {Object.keys(props.pool.custodies).length > 0 ? (
15 |
19 | ) : (
20 |
21 | )}
22 |
23 |
24 | {props.pool.name}
25 |
26 |
27 |
{props.pool.getTokenList().join(", ")}
28 |
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/ui/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ["./src/**/*.{js,ts,jsx,tsx}"],
3 | theme: {
4 | fontSize: {
5 | xs: "0.75rem",
6 | sm: "0.875rem",
7 | base: "1rem",
8 | lg: "1.125rem",
9 | xl: "1.25rem",
10 | "2xl": "1.5rem",
11 | "3xl": "1.875rem",
12 | "4xl": "2.25rem",
13 | "5xl": "3rem",
14 | "6xl": "4rem",
15 | },
16 | extend: {
17 | colors: {
18 | gray: {
19 | 100: "#f7fafc",
20 | 200: "#edf2f7",
21 | 300: "#e2e8f0",
22 | 400: "#cbd5e0",
23 | 500: "#a0aec0",
24 | 600: "#718096",
25 | 700: "#4a5568",
26 | 800: "#2d3748",
27 | 900: "#1a202c",
28 | },
29 | blue: {
30 | 100: "#ebf8ff",
31 | 200: "#bee3f8",
32 | 300: "#90cdf4",
33 | 400: "#63b3ed",
34 | 500: "#4299e1",
35 | 600: "#3182ce",
36 | 700: "#2b6cb0",
37 | 800: "#2c5282",
38 | 900: "#2a4365",
39 | },
40 | },
41 | },
42 | },
43 | };
44 |
--------------------------------------------------------------------------------
/ui/src/components/LoadingDots.tsx:
--------------------------------------------------------------------------------
1 | import { twMerge } from "tailwind-merge";
2 |
3 | interface Props {
4 | className?: string;
5 | numDots: number;
6 | style: "bounce" | "pulse";
7 | }
8 |
9 | export function LoadingDots(props: Props) {
10 | return (
11 |
20 | {Array.from({ length: props.numDots }).map((_, i) => (
21 |
37 | ))}
38 |
39 | );
40 | }
41 |
42 | LoadingDots.defaultProps = {
43 | numDots: 3,
44 | style: "bounce",
45 | };
46 |
--------------------------------------------------------------------------------
/ui/src/components/Positions/PositionValueDelta.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | formatValueDelta,
3 | formatValueDeltaPercentage,
4 | } from "@/utils/formatters";
5 | import { twMerge } from "tailwind-merge";
6 |
7 | interface Props {
8 | className?: string;
9 | valueDelta: number;
10 | valueDeltaPercentage: number;
11 | }
12 |
13 | export function PositionValueDelta(props: Props) {
14 | return (
15 |
16 |
0 ? "text-emerald-400" : "text-rose-400"
21 | )}
22 | >
23 | {props.valueDelta > 0 && "+"}
24 | {formatValueDelta(props.valueDelta)}
25 |
26 |
0 ? "bg-emerald-400" : "bg-rose-400"
34 | )}
35 | >
36 | {props.valueDeltaPercentage > 0 && "+"}
37 | {formatValueDeltaPercentage(props.valueDeltaPercentage)}%
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/programs/perpetuals/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "perpetuals"
3 | version = "0.1.0"
4 | description = "Solana Perpetuals Exchange"
5 | authors = ["Solana Maintainers "]
6 | repository = "https://github.com/askibin/perpetuals"
7 | categories = ["finance"]
8 | keywords = ["solana", "dex", "perpetuals", "futures", "exchange"]
9 | license = "Apache-2.0"
10 | homepage = "https://solana.com/"
11 | edition = "2021"
12 |
13 | [lib]
14 | crate-type = ["cdylib", "lib"]
15 | name = "perpetuals"
16 |
17 | [features]
18 | no-entrypoint = []
19 | no-idl = []
20 | no-log-ix-name = []
21 | cpi = ["no-entrypoint"]
22 | test = []
23 | default = []
24 |
25 | [profile.release]
26 | lto = true
27 | codegen-units = 1
28 | overflow-checks = true
29 |
30 | [dependencies]
31 | anchor-lang = {version = "0.26.0", features = ["init-if-needed"]}
32 | anchor-spl = "0.26.0"
33 | solana-program = "1.14.13"
34 | solana-security-txt = "1.1.0"
35 | pyth-sdk-solana = "0.7.0"
36 | ahash = "=0.7.6"
37 | num-traits = "0.2.15"
38 | num = "0.4.0"
39 |
40 | [dev-dependencies]
41 | solana-program-test = "1.14.13"
42 | solana-sdk = "1.14.13"
43 | tokio = { version = "1.0.0", features = ["macros"]}
44 | bonfida-test-utils = "0.2.1"
45 | bincode = "1.3.3"
--------------------------------------------------------------------------------
/ui/src/components/Molecules/PoolHeaders/TitleHeader.tsx:
--------------------------------------------------------------------------------
1 | import { PoolTokens } from "@/components/PoolTokens";
2 | import { PoolAccount } from "@/lib/PoolAccount";
3 | import { ACCOUNT_URL } from "@/utils/TransactionHandlers";
4 | import NewTab from "@carbon/icons-react/lib/NewTab";
5 | import { twMerge } from "tailwind-merge";
6 |
7 | interface Props {
8 | className?: string;
9 | iconClassName?: string;
10 | pool: PoolAccount;
11 | }
12 |
13 | export function TitleHeader(props: Props) {
14 | return (
15 |
16 |
17 |
21 |
{props.pool.name}
22 |
27 |
28 |
29 |
30 |
31 |
{props.pool.getTokenList().join(", ")}
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/programs/perpetuals/src/instructions/get_assets_under_management.rs:
--------------------------------------------------------------------------------
1 | //! GetAssetsUnderManagement instruction handler
2 |
3 | use {
4 | crate::state::{
5 | perpetuals::Perpetuals,
6 | pool::{AumCalcMode, Pool},
7 | },
8 | anchor_lang::prelude::*,
9 | };
10 |
11 | #[derive(Accounts)]
12 | pub struct GetAssetsUnderManagement<'info> {
13 | #[account(
14 | seeds = [b"perpetuals"],
15 | bump = perpetuals.perpetuals_bump
16 | )]
17 | pub perpetuals: Box>,
18 |
19 | #[account(
20 | seeds = [b"pool",
21 | pool.name.as_bytes()],
22 | bump = pool.bump
23 | )]
24 | pub pool: Box>,
25 | // remaining accounts:
26 | // pool.tokens.len() custody accounts (read-only, unsigned)
27 | // pool.tokens.len() custody oracles (read-only, unsigned)
28 | }
29 |
30 | #[derive(AnchorSerialize, AnchorDeserialize)]
31 | pub struct GetAssetsUnderManagementParams {}
32 |
33 | pub fn get_assets_under_management(
34 | ctx: Context,
35 | _params: &GetAssetsUnderManagementParams,
36 | ) -> Result {
37 | ctx.accounts.pool.get_assets_under_management_usd(
38 | AumCalcMode::EMA,
39 | ctx.remaining_accounts,
40 | ctx.accounts.perpetuals.get_time()?,
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/ui/src/components/Atoms/UserBalance.tsx:
--------------------------------------------------------------------------------
1 | import { TokenE } from "@/lib/Token";
2 | import { useGlobalStore } from "@/stores/store";
3 | import { useWallet } from "@solana/wallet-adapter-react";
4 |
5 | interface Props {
6 | token: TokenE;
7 | onClick: () => void;
8 | }
9 |
10 | export function UserBalance(props: Props) {
11 | const { publicKey } = useWallet();
12 | const userData = useGlobalStore((state) => state.userData);
13 |
14 | let balance = userData.tokenBalances[props.token];
15 | if (!publicKey) {
16 | return (
17 |
20 | );
21 | }
22 | if (balance) {
23 | return (
24 |
28 |
{balance.toFixed(4)}
29 |
{props.token}
30 |
Balance
31 |
32 | );
33 | } else {
34 | return (
35 |
36 |
0
37 |
{props.token}
38 |
Balance
39 |
40 | );
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/ui/src/components/NavbarLink.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { useRouter } from "next/router";
3 | import { cloneElement } from "react";
4 | import { twMerge } from "tailwind-merge";
5 |
6 | interface Props extends React.AnchorHTMLAttributes {
7 | href: string;
8 | icon: JSX.Element;
9 | }
10 |
11 | export function NavbarLink(props: Props) {
12 | const router = useRouter();
13 |
14 | const currentPath = router.pathname;
15 | const selected = currentPath.startsWith(props.href);
16 |
17 | return (
18 |
37 | {props.children}
38 | {cloneElement(props.icon, {
39 | className: twMerge(
40 | props.icon.props.className,
41 | "block",
42 | "fill-current",
43 | "h-4",
44 | "w-4",
45 | "md:hidden"
46 | ),
47 | })}
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/ui/src/components/Chart/DailyStats.tsx:
--------------------------------------------------------------------------------
1 | import { TokenE } from "@/lib/Token";
2 | import { useGlobalStore } from "@/stores/store";
3 | import { formatNumberCommas } from "@/utils/formatters";
4 | import { twMerge } from "tailwind-merge";
5 |
6 | interface DailyStatsProps {
7 | className?: string;
8 | token: TokenE;
9 | }
10 |
11 | export function DailyStats(props: DailyStatsProps) {
12 | const stats = useGlobalStore((state) => state.priceStats);
13 |
14 | if (Object.values(stats).length === 0) return sdf
;
15 |
16 | return (
17 |
20 |
21 |
Current Price
22 |
23 | ${formatNumberCommas(stats[props.token].currentPrice)}
24 |
25 |
26 |
27 |
24h Change
28 |
0 && "text-emerald-400"
34 | )}
35 | >
36 | ${formatNumberCommas(stats[props.token].change24hr)}
37 |
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/ui/src/components/Icons/LoadingSpinner.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export const LoadingSpinner = (props: React.SVGProps) => {
4 | const {className, ...rest} = props
5 | return (
6 |
7 |
8 |
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/ui/src/pages/trade/[pair].tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 | import { TradeLayout } from "@/components/Layouts/TradeLayout";
3 | import { CandlestickChart } from "@/components/Chart/CandlestickChart";
4 | import { TradeSidebar } from "@/components/TradeSidebar";
5 | import { asToken } from "@/lib/Token";
6 | import { Positions } from "@/components/Positions";
7 |
8 | function getToken(pair: string) {
9 | const [token, _] = pair.split("-");
10 | return asToken(token || "");
11 | }
12 |
13 | function getComparisonCurrency() {
14 | return "usd" as const;
15 | }
16 |
17 | export default function Page() {
18 | const router = useRouter();
19 | const { pair } = router.query;
20 |
21 | if (!pair) {
22 | return <>>;
23 | }
24 |
25 | // @ts-ignore
26 | let token: ReturnType = asToken(pair.split("-")[0]);
27 | let currency: ReturnType =
28 | getComparisonCurrency();
29 |
30 | if (pair && Array.isArray(pair)) {
31 | const tokenAndCurrency = pair[0];
32 |
33 | if (tokenAndCurrency) {
34 | token = getToken(tokenAndCurrency);
35 | currency = getComparisonCurrency();
36 | }
37 | }
38 |
39 | return (
40 |
41 |
42 |
43 |
44 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/ui/src/pages/pools/[poolName].tsx:
--------------------------------------------------------------------------------
1 | import PoolBackButton from "@/components/Atoms/PoolBackButton";
2 | import { LoadingSpinner } from "@/components/Icons/LoadingSpinner";
3 | import { PoolLayout } from "@/components/Layouts/PoolLayout";
4 | import { TitleHeader } from "@/components/Molecules/PoolHeaders/TitleHeader";
5 | import LiquidityCard from "@/components/PoolModal/LiquidityCard";
6 | import PoolGeneralStats from "@/components/PoolModal/PoolGeneralStats";
7 | import PoolTokenStats from "@/components/PoolModal/PoolTokenStats";
8 | import { useGlobalStore } from "@/stores/store";
9 | import { useRouter } from "next/router";
10 |
11 | export default function SinglePool() {
12 | const router = useRouter();
13 |
14 | const poolData = useGlobalStore((state) => state.poolData);
15 | let pool = poolData[router.query.poolName as string];
16 |
17 | if (!pool) {
18 | return ;
19 | } else {
20 | return (
21 |
22 |
30 |
34 |
35 |
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ui/src/hooks/storeHelpers/fetchPrices.ts:
--------------------------------------------------------------------------------
1 | import { TOKEN_LIST, getTokenId } from "@/lib/Token";
2 | import { PriceStats } from "@/lib/types";
3 |
4 | type FetchedData = {
5 | [key: string]: {
6 | usd: number;
7 | usd_24h_vol: number;
8 | usd_24h_change: number;
9 | };
10 | };
11 |
12 | export function fetchAllStats(): PriceStats {
13 | let stats = fetch(
14 | `https://api.coingecko.com/api/v3/simple/price?ids=${TOKEN_LIST.map(
15 | getTokenId
16 | ).join(
17 | ","
18 | )}&vs_currencies=USD&include_24hr_vol=true&include_24hr_change=true`
19 | )
20 | .then((resp) => resp.json())
21 | .then((data: FetchedData) => {
22 | const allStats = TOKEN_LIST.reduce((acc, token) => {
23 | const tokenData = data[getTokenId(token)];
24 |
25 | acc[token] = {
26 | change24hr: tokenData!.usd_24h_change,
27 | currentPrice: tokenData!.usd,
28 | high24hr: 0,
29 | low24hr: 0,
30 | };
31 |
32 | return acc;
33 | }, {} as PriceStats);
34 |
35 | return allStats;
36 | })
37 | .catch(() => {
38 | console.log("caught data fetching error");
39 | const allStats = TOKEN_LIST.reduce((acc, token) => {
40 | acc[token] = {
41 | change24hr: 0,
42 | currentPrice: 0,
43 | high24hr: 0,
44 | low24hr: 0,
45 | };
46 |
47 | return acc;
48 | }, {} as PriceStats);
49 |
50 | return allStats;
51 | });
52 |
53 | return stats;
54 | }
55 |
--------------------------------------------------------------------------------
/ui/src/components/Positions/index.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingSpinner } from "@/components/Icons/LoadingSpinner";
2 | import { LoadingDots } from "@/components/LoadingDots";
3 | import { ExistingPositions } from "@/components/Positions/ExistingPositions";
4 | import { NoPositions } from "@/components/Positions/NoPositions";
5 | import { useGlobalStore } from "@/stores/store";
6 | import { useWallet } from "@solana/wallet-adapter-react";
7 |
8 | interface Props {
9 | className?: string;
10 | }
11 |
12 | export function Positions(props: Props) {
13 | const { publicKey } = useWallet();
14 |
15 | const positionData = useGlobalStore((state) => state.positionData);
16 |
17 | if (positionData.status === "pending") {
18 | return ;
19 | }
20 |
21 | if (!publicKey) {
22 | return (
23 |
30 | );
31 | }
32 |
33 | return (
34 |
35 |
36 | My Positions
37 | {positionData.status === "pending" && (
38 |
39 | )}
40 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/ui/src/hooks/storeHelpers/fetchPositions.ts:
--------------------------------------------------------------------------------
1 | import { CustodyAccount } from "@/lib/CustodyAccount";
2 | import { PositionAccount } from "@/lib/PositionAccount";
3 | import { Position } from "@/lib/types";
4 | import { getPerpetualProgramAndProvider } from "@/utils/constants";
5 | import { PublicKey } from "@solana/web3.js";
6 |
7 | interface Pending {
8 | status: "pending";
9 | }
10 |
11 | interface Failure {
12 | status: "failure";
13 | error: Error;
14 | }
15 |
16 | interface Success {
17 | status: "success";
18 | data: Record;
19 | }
20 |
21 | interface FetchPosition {
22 | account: Position;
23 | publicKey: PublicKey;
24 | }
25 |
26 | export type PositionRequest = Pending | Failure | Success;
27 |
28 | export async function getPositionData(
29 | custodyInfos: Record
30 | ): Promise {
31 | let { perpetual_program } = await getPerpetualProgramAndProvider();
32 |
33 | // @ts-ignore
34 | let fetchedPositions: FetchPosition[] =
35 | await perpetual_program.account.position.all();
36 |
37 | let positionInfos: Record = fetchedPositions.reduce(
38 | (acc: Record, position: FetchPosition) => (
39 | (acc[position.publicKey.toString()] = new PositionAccount(
40 | position.account,
41 | position.publicKey,
42 | custodyInfos
43 | )),
44 | acc
45 | ),
46 | {}
47 | );
48 |
49 | return {
50 | status: "success",
51 | data: positionInfos,
52 | };
53 | }
54 |
--------------------------------------------------------------------------------
/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "module": "esnext",
5 | "moduleResolution": "node",
6 | "resolveJsonModule": true,
7 | "removeComments": true,
8 | "preserveConstEnums": true,
9 | "strict": true,
10 | "alwaysStrict": true,
11 | "strictNullChecks": true,
12 | "noUncheckedIndexedAccess": true,
13 | "noImplicitAny": true,
14 | "noImplicitReturns": true,
15 | "noImplicitThis": true,
16 | "noUnusedLocals": true,
17 | "noUnusedParameters": true,
18 | "allowUnreachableCode": false,
19 | "noFallthroughCasesInSwitch": true,
20 | "target": "es5",
21 | "outDir": "out",
22 | "declaration": true,
23 | "sourceMap": true,
24 | "esModuleInterop": true,
25 | "allowSyntheticDefaultImports": true,
26 | "allowJs": false,
27 | "skipLibCheck": true,
28 | "forceConsistentCasingInFileNames": true,
29 | "jsx": "preserve",
30 | "noEmit": true,
31 | "isolatedModules": true,
32 | "incremental": true,
33 | "baseUrl": ".",
34 | "paths": {
35 | "@/components/*": ["./src/components/*"],
36 | "@/utils/*": ["./src/utils/*"],
37 | "@/styles/*": ["./src/styles/*"],
38 | "@/hooks/*": ["./src/hooks/*"],
39 | "@/target/*": ["./src/target/*"],
40 | "@/lib/*": ["src/lib/*"],
41 | "@/stores/*": ["src/stores/*"],
42 | }
43 | },
44 | "exclude": ["./out/**/*", "./node_modules/**/*", "**/*.test.ts"],
45 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
46 | }
47 |
--------------------------------------------------------------------------------
/ui/src/components/Positions/PoolPositionHeader.tsx:
--------------------------------------------------------------------------------
1 | import { PoolTokens } from "@/components/PoolTokens";
2 | import { PositionColumn } from "@/components/Positions/PositionColumn";
3 | import { PositionAccount } from "@/lib/PositionAccount";
4 | import { useGlobalStore } from "@/stores/store";
5 |
6 | interface Props {
7 | className?: string;
8 | positions: PositionAccount[];
9 | }
10 |
11 | export default function PoolPositionHeader(props: Props) {
12 | const allTokens = props.positions.map((position) => {
13 | return position.token;
14 | });
15 |
16 | const tokens = Array.from(new Set(allTokens));
17 |
18 | const poolData = useGlobalStore((state) => state.poolData);
19 |
20 | if (!props.positions[0]) return No Positions
;
21 |
22 | return (
23 | <>
24 |
25 |
26 |
27 |
28 | {poolData[props.positions[0].pool.toString()]?.name}
29 |
30 |
31 |
32 | Leverage
33 | Net Value
34 | Collateral
35 | Entry Price
36 | Mark Price
37 | Liq. Price
38 | >
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/programs/perpetuals/src/state/position.rs:
--------------------------------------------------------------------------------
1 | use {
2 | crate::{math, state::perpetuals::Perpetuals},
3 | anchor_lang::prelude::*,
4 | };
5 |
6 | #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Debug)]
7 | pub enum Side {
8 | None,
9 | Long,
10 | Short,
11 | }
12 |
13 | impl Default for Side {
14 | fn default() -> Self {
15 | Self::None
16 | }
17 | }
18 |
19 | #[derive(Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize, Debug)]
20 | pub enum CollateralChange {
21 | None,
22 | Add,
23 | Remove,
24 | }
25 |
26 | impl Default for CollateralChange {
27 | fn default() -> Self {
28 | Self::None
29 | }
30 | }
31 |
32 | #[account]
33 | #[derive(Default, Debug)]
34 | pub struct Position {
35 | pub owner: Pubkey,
36 | pub pool: Pubkey,
37 | pub custody: Pubkey,
38 |
39 | pub open_time: i64,
40 | pub update_time: i64,
41 | pub side: Side,
42 | pub price: u64,
43 | pub size_usd: u64,
44 | pub collateral_usd: u64,
45 | pub unrealized_profit_usd: u64,
46 | pub unrealized_loss_usd: u64,
47 | pub cumulative_interest_snapshot: u128,
48 | pub locked_amount: u64,
49 | pub collateral_amount: u64,
50 |
51 | pub bump: u8,
52 | }
53 |
54 | impl Position {
55 | pub const LEN: usize = 8 + std::mem::size_of::();
56 |
57 | pub fn get_initial_leverage(&self) -> Result {
58 | math::checked_as_u64(math::checked_div(
59 | math::checked_mul(self.size_usd as u128, Perpetuals::BPS_POWER)?,
60 | self.collateral_usd as u128,
61 | )?)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/programs/perpetuals/src/instructions/set_admin_signers.rs:
--------------------------------------------------------------------------------
1 | //! SetAdminSigners instruction handler
2 |
3 | use {
4 | crate::state::multisig::{AdminInstruction, Multisig},
5 | anchor_lang::prelude::*,
6 | };
7 |
8 | #[derive(Accounts)]
9 | pub struct SetAdminSigners<'info> {
10 | #[account()]
11 | pub admin: Signer<'info>,
12 |
13 | #[account(
14 | mut,
15 | seeds = [b"multisig"],
16 | bump = multisig.load()?.bump
17 | )]
18 | pub multisig: AccountLoader<'info, Multisig>,
19 | // remaining accounts: 1 to Multisig::MAX_SIGNERS admin signers (read-only, unsigned)
20 | }
21 |
22 | #[derive(AnchorSerialize, AnchorDeserialize)]
23 | pub struct SetAdminSignersParams {
24 | pub min_signatures: u8,
25 | }
26 |
27 | pub fn set_admin_signers<'info>(
28 | ctx: Context<'_, '_, '_, 'info, SetAdminSigners<'info>>,
29 | params: &SetAdminSignersParams,
30 | ) -> Result {
31 | // validate signatures
32 | let mut multisig = ctx.accounts.multisig.load_mut()?;
33 |
34 | let signatures_left = multisig.sign_multisig(
35 | &ctx.accounts.admin,
36 | &Multisig::get_account_infos(&ctx)[1..],
37 | &Multisig::get_instruction_data(AdminInstruction::SetAdminSigners, params)?,
38 | )?;
39 | if signatures_left > 0 {
40 | msg!(
41 | "Instruction has been signed but more signatures are required: {}",
42 | signatures_left
43 | );
44 | return Ok(signatures_left);
45 | }
46 |
47 | // set new admin signers
48 | multisig.set_signers(ctx.remaining_accounts, params.min_signatures)?;
49 |
50 | Ok(0)
51 | }
52 |
--------------------------------------------------------------------------------
/ui/src/components/TradeSidebar/TradeSwapDetails.tsx:
--------------------------------------------------------------------------------
1 | import { TokenE } from "@/lib/Token";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | function formatPrice(num: number) {
5 | const formatter = Intl.NumberFormat("en", {
6 | maximumFractionDigits: 2,
7 | minimumFractionDigits: 2,
8 | });
9 | return formatter.format(num);
10 | }
11 |
12 | interface Props {
13 | availableLiquidity: number;
14 | className?: string;
15 | payToken: TokenE;
16 | payTokenPrice: number;
17 | receiveToken: TokenE;
18 | receiveTokenPrice: number;
19 | }
20 |
21 | export function TradeSwapDetails(props: Props) {
22 | return (
23 |
24 |
25 | {[
26 | {
27 | label: `${props.payToken} Price`,
28 | value: `$${formatPrice(props.payTokenPrice)}`,
29 | },
30 | {
31 | label: `${props.receiveToken} Price`,
32 | value: `$${formatPrice(props.receiveTokenPrice)}`,
33 | },
34 | {
35 | label: "Available Liquidity",
36 | value: `$${formatPrice(props.availableLiquidity)}`,
37 | },
38 | ].map(({ label, value }, i) => (
39 |
47 |
{label}
48 |
{value}
49 |
50 | ))}
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/ui/src/components/Chart/CandlestickChart.tsx:
--------------------------------------------------------------------------------
1 | import { ChartCurrency } from "@/components/Chart/ChartCurrency";
2 | import { DailyStats } from "@/components/Chart/DailyStats";
3 | import { getSymbol, TokenE } from "@/lib/Token";
4 | import dynamic from "next/dynamic";
5 |
6 | // @ts-ignore
7 | const TradingViewWidget = dynamic(import("react-tradingview-widget"), {
8 | ssr: false,
9 | });
10 |
11 | interface Props {
12 | className?: string;
13 | comparisonCurrency: "usd";
14 | token: TokenE;
15 | }
16 |
17 | export function CandlestickChart(props: Props) {
18 | return (
19 |
20 |
21 |
25 |
26 |
27 |
45 |
46 | );
47 | }
48 |
49 | CandlestickChart.defaultProps = {
50 | token: TokenE.SOL,
51 | comparisonCurrency: "usd",
52 | };
53 |
--------------------------------------------------------------------------------
/ui/src/stores/store.tsx:
--------------------------------------------------------------------------------
1 | import { PositionRequest } from "@/hooks/storeHelpers/fetchPositions";
2 | import { CustodyAccount } from "@/lib/CustodyAccount";
3 | import { PoolAccount } from "@/lib/PoolAccount";
4 | import { Custody, PriceStats } from "@/lib/types";
5 | import { UserAccount } from "@/lib/UserAccount";
6 | import { create } from "zustand";
7 | import { devtools } from "zustand/middleware";
8 |
9 | interface StoreState {
10 | positionData: PositionRequest;
11 | setPositionData: (position: PositionRequest) => void;
12 |
13 | poolData: Record;
14 | setPoolData: (pool: Record) => void;
15 |
16 | custodyData: Record;
17 | setCustodyData: (custody: Record) => void;
18 |
19 | userData: UserAccount;
20 | setUserData: (user: UserAccount) => void;
21 |
22 | priceStats: PriceStats;
23 | setPriceStats: (stats: PriceStats) => void;
24 | }
25 |
26 | export const useGlobalStore = create()(
27 | devtools((set, get) => ({
28 | devtools: false,
29 |
30 | positionData: {
31 | status: "pending",
32 | },
33 | setPositionData: (position: PositionRequest) =>
34 | set({ positionData: position }),
35 |
36 | poolData: {},
37 | setPoolData: (poolObjs: Record) =>
38 | set({ poolData: poolObjs }),
39 |
40 | custodyData: {},
41 | setCustodyData: (custody: Record) =>
42 | set({ custodyData: custody }),
43 |
44 | userData: new UserAccount(),
45 | setUserData: (user: UserAccount) => set({ userData: user }),
46 |
47 | priceStats: {},
48 | setPriceStats: (stats: PriceStats) => set({ priceStats: stats }),
49 | }))
50 | );
51 |
--------------------------------------------------------------------------------
/ui/src/utils/organizers.ts:
--------------------------------------------------------------------------------
1 | import { PositionRequest } from "@/hooks/storeHelpers/fetchPositions";
2 | import { PositionAccount } from "@/lib/PositionAccount";
3 | import { PublicKey } from "@solana/web3.js";
4 |
5 | export function getPoolSortedPositions(
6 | positionData: PositionRequest,
7 | user?: PublicKey
8 | ) {
9 | let sortedPositions: Record = {};
10 |
11 | if (
12 | positionData.status === "success" &&
13 | Object.values(positionData.data).length > 0
14 | ) {
15 | Object.values(positionData.data).forEach((position: PositionAccount) => {
16 | if (user && position.owner.toBase58() !== user.toBase58()) {
17 | return;
18 | }
19 |
20 | let pool = position.pool.toString();
21 |
22 | if (!sortedPositions[pool]) {
23 | sortedPositions[pool] = [];
24 | }
25 |
26 | sortedPositions[pool].push(position);
27 | });
28 | }
29 |
30 | return sortedPositions;
31 | }
32 |
33 | export function getUserPositionTokens(
34 | positionData: PositionRequest,
35 | user: PublicKey
36 | ): Record {
37 | let positionTokens: Record = {};
38 |
39 | if (
40 | positionData.status === "success" &&
41 | Object.values(positionData.data).length > 0 &&
42 | user
43 | ) {
44 | Object.values(positionData.data).forEach((position: PositionAccount) => {
45 | if (position.owner.toBase58() !== user.toBase58()) {
46 | return;
47 | }
48 |
49 | let tok = position.token;
50 |
51 | if (!positionTokens[tok]) {
52 | positionTokens[tok] = 1;
53 | }
54 | });
55 | }
56 |
57 | return positionTokens;
58 | }
59 |
60 | export function countDictList(dict: Record) {
61 | return Object.values(dict).reduce((acc, val) => acc + val.length, 0);
62 | }
63 |
--------------------------------------------------------------------------------
/ui/src/components/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import ChartCandlestickIcon from "@carbon/icons-react/lib/ChartCandlestick";
2 | import CircleDash from "@carbon/icons-react/lib/CircleDash";
3 | import StoragePoolIcon from "@carbon/icons-react/lib/StoragePool";
4 | import dynamic from "next/dynamic";
5 | import Link from "next/link";
6 | import { twMerge } from "tailwind-merge";
7 |
8 | import UserAdmin from "@carbon/icons-react/lib/UserAdmin";
9 | import { NavbarLink } from "./NavbarLink";
10 |
11 | const WalletMultiButtonDynamic = dynamic(
12 | async () =>
13 | (await import("@solana/wallet-adapter-react-ui")).WalletMultiButton,
14 | { ssr: false }
15 | );
16 |
17 | export const Navbar = () => {
18 | return (
19 |
34 |
35 |
36 | APP NAME
37 |
38 |
39 | }>
40 | Trade
41 |
42 | }>
43 | Pools
44 |
45 | }>
46 | Admin
47 |
48 |
49 |
50 |
Connect to DEVNET!
51 |
52 |
53 |
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/programs/perpetuals/src/instructions.rs:
--------------------------------------------------------------------------------
1 | // admin instructions
2 | pub mod add_custody;
3 | pub mod add_pool;
4 | pub mod init;
5 | pub mod remove_custody;
6 | pub mod remove_pool;
7 | pub mod set_admin_signers;
8 | pub mod set_custody_config;
9 | pub mod set_permissions;
10 | pub mod upgrade_custody;
11 | pub mod withdraw_fees;
12 | pub mod withdraw_sol_fees;
13 |
14 | // test instructions
15 | pub mod set_test_oracle_price;
16 | pub mod set_test_time;
17 | pub mod test_init;
18 |
19 | // public instructions
20 | pub mod add_collateral;
21 | pub mod add_liquidity;
22 | pub mod close_position;
23 | pub mod get_add_liquidity_amount_and_fee;
24 | pub mod get_assets_under_management;
25 | pub mod get_entry_price_and_fee;
26 | pub mod get_exit_price_and_fee;
27 | pub mod get_liquidation_price;
28 | pub mod get_liquidation_state;
29 | pub mod get_oracle_price;
30 | pub mod get_pnl;
31 | pub mod get_remove_liquidity_amount_and_fee;
32 | pub mod get_swap_amount_and_fees;
33 | pub mod liquidate;
34 | pub mod open_position;
35 | pub mod remove_collateral;
36 | pub mod remove_liquidity;
37 | pub mod swap;
38 |
39 | // bring everything in scope
40 | pub use {
41 | add_collateral::*, add_custody::*, add_liquidity::*, add_pool::*, close_position::*,
42 | get_add_liquidity_amount_and_fee::*, get_assets_under_management::*,
43 | get_entry_price_and_fee::*, get_exit_price_and_fee::*, get_liquidation_price::*,
44 | get_liquidation_state::*, get_oracle_price::*, get_pnl::*,
45 | get_remove_liquidity_amount_and_fee::*, get_swap_amount_and_fees::*, init::*, liquidate::*,
46 | open_position::*, remove_collateral::*, remove_custody::*, remove_liquidity::*, remove_pool::*,
47 | set_admin_signers::*, set_custody_config::*, set_permissions::*, set_test_oracle_price::*,
48 | set_test_time::*, swap::*, test_init::*, upgrade_custody::*, withdraw_fees::*,
49 | withdraw_sol_fees::*,
50 | };
51 |
--------------------------------------------------------------------------------
/programs/perpetuals/src/instructions/get_oracle_price.rs:
--------------------------------------------------------------------------------
1 | //! GetOraclePrice instruction handler
2 |
3 | use {
4 | crate::state::{custody::Custody, oracle::OraclePrice, perpetuals::Perpetuals, pool::Pool},
5 | anchor_lang::prelude::*,
6 | };
7 |
8 | #[derive(Accounts)]
9 | pub struct GetOraclePrice<'info> {
10 | #[account(
11 | seeds = [b"perpetuals"],
12 | bump = perpetuals.perpetuals_bump
13 | )]
14 | pub perpetuals: Box>,
15 |
16 | #[account(
17 | seeds = [b"pool",
18 | pool.name.as_bytes()],
19 | bump = pool.bump
20 | )]
21 | pub pool: Box>,
22 |
23 | #[account(
24 | seeds = [b"custody",
25 | pool.key().as_ref(),
26 | custody.mint.as_ref()],
27 | bump = custody.bump
28 | )]
29 | pub custody: Box>,
30 |
31 | /// CHECK: oracle account for the collateral token
32 | #[account(
33 | constraint = custody_oracle_account.key() == custody.oracle.oracle_account
34 | )]
35 | pub custody_oracle_account: AccountInfo<'info>,
36 | }
37 |
38 | #[derive(AnchorSerialize, AnchorDeserialize)]
39 | pub struct GetOraclePriceParams {
40 | ema: bool,
41 | }
42 |
43 | pub fn get_oracle_price(
44 | ctx: Context,
45 | params: &GetOraclePriceParams,
46 | ) -> Result {
47 | let custody = ctx.accounts.custody.as_mut();
48 | let curtime = ctx.accounts.perpetuals.get_time()?;
49 |
50 | let price = OraclePrice::new_from_oracle(
51 | custody.oracle.oracle_type,
52 | &ctx.accounts.custody_oracle_account.to_account_info(),
53 | custody.oracle.max_price_error,
54 | custody.oracle.max_price_age_sec,
55 | curtime,
56 | params.ema,
57 | )?;
58 |
59 | Ok(price
60 | .scale_to_exponent(-(Perpetuals::PRICE_DECIMALS as i32))?
61 | .price)
62 | }
63 |
--------------------------------------------------------------------------------
/programs/perpetuals/src/instructions/set_test_time.rs:
--------------------------------------------------------------------------------
1 | //! SetTestTime instruction handler
2 |
3 | use {
4 | crate::{
5 | error::PerpetualsError,
6 | state::{
7 | multisig::{AdminInstruction, Multisig},
8 | perpetuals::Perpetuals,
9 | },
10 | },
11 | anchor_lang::prelude::*,
12 | };
13 |
14 | #[derive(Accounts)]
15 | pub struct SetTestTime<'info> {
16 | #[account()]
17 | pub admin: Signer<'info>,
18 |
19 | #[account(
20 | mut,
21 | seeds = [b"multisig"],
22 | bump = multisig.load()?.bump
23 | )]
24 | pub multisig: AccountLoader<'info, Multisig>,
25 |
26 | #[account(
27 | mut,
28 | seeds = [b"perpetuals"],
29 | bump = perpetuals.perpetuals_bump
30 | )]
31 | pub perpetuals: Box>,
32 | }
33 |
34 | #[derive(AnchorSerialize, AnchorDeserialize)]
35 | pub struct SetTestTimeParams {
36 | pub time: i64,
37 | }
38 |
39 | pub fn set_test_time<'info>(
40 | ctx: Context<'_, '_, '_, 'info, SetTestTime<'info>>,
41 | params: &SetTestTimeParams,
42 | ) -> Result {
43 | if !cfg!(feature = "test") {
44 | return err!(PerpetualsError::InvalidEnvironment);
45 | }
46 |
47 | // validate signatures
48 | let mut multisig = ctx.accounts.multisig.load_mut()?;
49 |
50 | let signatures_left = multisig.sign_multisig(
51 | &ctx.accounts.admin,
52 | &Multisig::get_account_infos(&ctx)[1..],
53 | &Multisig::get_instruction_data(AdminInstruction::SetTestTime, params)?,
54 | )?;
55 | if signatures_left > 0 {
56 | msg!(
57 | "Instruction has been signed but more signatures are required: {}",
58 | signatures_left
59 | );
60 | return Ok(signatures_left);
61 | }
62 |
63 | // update time data
64 | if cfg!(feature = "test") {
65 | ctx.accounts.perpetuals.inception_time = params.time;
66 | }
67 |
68 | Ok(0)
69 | }
70 |
--------------------------------------------------------------------------------
/ui/src/utils/formatters.ts:
--------------------------------------------------------------------------------
1 | import { BN } from "@project-serum/anchor";
2 |
3 | export function formatNumberCommas(num: number | BN | null) {
4 | if (typeof num === "bigint") {
5 | return Number(num).toLocaleString(undefined, {
6 | minimumFractionDigits: 2,
7 | maximumFractionDigits: 2,
8 | });
9 | } else if (typeof num === "number") {
10 | return num.toLocaleString(undefined, {
11 | minimumFractionDigits: 2,
12 | maximumFractionDigits: 2,
13 | });
14 | } else {
15 | return null;
16 | }
17 | }
18 |
19 | export function formatNumber(num: number) {
20 | const formatter = Intl.NumberFormat("en", {
21 | maximumFractionDigits: 2,
22 | minimumFractionDigits: 2,
23 | });
24 | return formatter.format(num);
25 | }
26 |
27 | export function formatNumberLessThan(num: number) {
28 | const formatter = Intl.NumberFormat("en", {
29 | maximumFractionDigits: 2,
30 | minimumFractionDigits: 2,
31 | });
32 |
33 | if (num < 0.01) {
34 | return "<$0.01";
35 | } else {
36 | return "$" + formatter.format(num);
37 | }
38 | }
39 |
40 | export function formatPrice(num: number) {
41 | const formatter = Intl.NumberFormat("en", {
42 | maximumFractionDigits: 2,
43 | minimumFractionDigits: 2,
44 | });
45 | return formatter.format(num);
46 | }
47 |
48 | export function formatFees(num: number) {
49 | const formatter = Intl.NumberFormat("en", {
50 | maximumFractionDigits: 5,
51 | minimumFractionDigits: 3,
52 | });
53 | return formatter.format(num);
54 | }
55 |
56 | export function formatValueDelta(num: number) {
57 | const formatter = new Intl.NumberFormat("en", {
58 | maximumFractionDigits: 4,
59 | minimumFractionDigits: 4,
60 | });
61 | return formatter.format(num);
62 | }
63 |
64 | export function formatValueDeltaPercentage(num: number) {
65 | const formatter = new Intl.NumberFormat("en", {
66 | maximumFractionDigits: 2,
67 | minimumFractionDigits: 2,
68 | });
69 | return formatter.format(num);
70 | }
71 |
--------------------------------------------------------------------------------
/programs/perpetuals/src/error.rs:
--------------------------------------------------------------------------------
1 | //! Error types
2 |
3 | use anchor_lang::prelude::*;
4 |
5 | #[error_code]
6 | pub enum PerpetualsError {
7 | #[msg("Account is not authorized to sign this instruction")]
8 | MultisigAccountNotAuthorized,
9 | #[msg("Account has already signed this instruction")]
10 | MultisigAlreadySigned,
11 | #[msg("This instruction has already been executed")]
12 | MultisigAlreadyExecuted,
13 | #[msg("Overflow in arithmetic operation")]
14 | MathOverflow,
15 | #[msg("Unsupported price oracle")]
16 | UnsupportedOracle,
17 | #[msg("Invalid oracle account")]
18 | InvalidOracleAccount,
19 | #[msg("Invalid oracle state")]
20 | InvalidOracleState,
21 | #[msg("Stale oracle price")]
22 | StaleOraclePrice,
23 | #[msg("Invalid oracle price")]
24 | InvalidOraclePrice,
25 | #[msg("Instruction is not allowed in production")]
26 | InvalidEnvironment,
27 | #[msg("Invalid pool state")]
28 | InvalidPoolState,
29 | #[msg("Invalid custody state")]
30 | InvalidCustodyState,
31 | #[msg("Invalid position state")]
32 | InvalidPositionState,
33 | #[msg("Invalid perpetuals config")]
34 | InvalidPerpetualsConfig,
35 | #[msg("Invalid pool config")]
36 | InvalidPoolConfig,
37 | #[msg("Invalid custody config")]
38 | InvalidCustodyConfig,
39 | #[msg("Insufficient token amount returned")]
40 | InsufficientAmountReturned,
41 | #[msg("Price slippage limit exceeded")]
42 | MaxPriceSlippage,
43 | #[msg("Position leverage limit exceeded")]
44 | MaxLeverage,
45 | #[msg("Custody amount limit exceeded")]
46 | CustodyAmountLimit,
47 | #[msg("Position amount limit exceeded")]
48 | PositionAmountLimit,
49 | #[msg("Token ratio out of range")]
50 | TokenRatioOutOfRange,
51 | #[msg("Token is not supported")]
52 | UnsupportedToken,
53 | #[msg("Instruction is not allowed at this time")]
54 | InstructionNotAllowed,
55 | #[msg("Token utilization limit exceeded")]
56 | MaxUtilization,
57 | }
58 |
--------------------------------------------------------------------------------
/ui/src/components/TradeSidebar/index.tsx:
--------------------------------------------------------------------------------
1 | import { SidebarTab } from "@/components/SidebarTab";
2 | import { TradePosition } from "@/components/TradeSidebar/TradePosition";
3 | import { TradeSwap } from "@/components/TradeSidebar/TradeSwap";
4 | import { Side } from "@/lib/types";
5 | import ArrowsHorizontalIcon from "@carbon/icons-react/lib/ArrowsHorizontal";
6 | import GrowthIcon from "@carbon/icons-react/lib/Growth";
7 | import { useState } from "react";
8 | import { twMerge } from "tailwind-merge";
9 |
10 | interface Props {
11 | className?: string;
12 | }
13 |
14 | export function TradeSidebar(props: Props) {
15 | const [side, setSide] = useState(Side.Long);
16 |
17 | return (
18 |
19 |
Place a Market Order
20 |
23 |
24 |
setSide(Side.Long)}
27 | >
28 |
29 | Long
30 |
31 |
setSide(Side.Short)}
34 | >
35 |
36 | Short
37 |
38 |
setSide(Side.Swap)}
41 | >
42 |
43 | Swap
44 |
45 |
46 | {side === Side.Long && (
47 |
48 | )}
49 | {side === Side.Short && (
50 |
51 | )}
52 | {side === Side.Swap &&
}
53 |
54 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/ui/src/components/SolidButton.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingSpinner } from "@/components/Icons/LoadingSpinner";
2 | import { forwardRef, useState } from "react";
3 | import { twMerge } from "tailwind-merge";
4 |
5 | interface Props extends React.ButtonHTMLAttributes {
6 | pending?: boolean;
7 | }
8 |
9 | export const SolidButton = forwardRef(
10 | function SolidButton(props, ref) {
11 | const { ...rest } = props;
12 | const [loading, setLoading] = useState(false);
13 |
14 | const handleClick = async (e: any) => {
15 | setLoading(true);
16 | try {
17 | await rest.onClick?.(e);
18 | } catch (error) {
19 | console.error("ButtonWithLoading onClick error:", error);
20 | }
21 | setLoading(false);
22 | };
23 |
24 | return (
25 | {
49 | handleClick(e);
50 | }}
51 | >
52 |
64 | {rest.children}
65 |
66 | {loading && }
67 |
68 | );
69 | }
70 | );
71 |
--------------------------------------------------------------------------------
/ui/src/hooks/storeHelpers/fetchUserData.ts:
--------------------------------------------------------------------------------
1 | import { PoolAccount } from "@/lib/PoolAccount";
2 | import { TokenE } from "@/lib/Token";
3 | import { UserAccount } from "@/lib/UserAccount";
4 | import { fetchLPBalance, fetchTokenBalance } from "@/utils/retrieveData";
5 | import { Connection, PublicKey } from "@solana/web3.js";
6 |
7 | export default async function getUserLpAll(
8 | connection: Connection,
9 | publicKey: PublicKey,
10 | poolData: Record
11 | ): Promise> {
12 | let lpTokenAccounts: Record = {};
13 | let promises = Object.values(poolData).map(async (pool) => {
14 | lpTokenAccounts[pool.address.toString()] = await fetchLPBalance(
15 | pool.getLpTokenMint(),
16 | publicKey,
17 | connection
18 | );
19 | });
20 |
21 | await Promise.all(promises);
22 |
23 | return lpTokenAccounts;
24 | }
25 |
26 | export async function getUserLpSingle() {}
27 |
28 | export async function getUserTokenAll(
29 | connection: Connection,
30 | publicKey: PublicKey,
31 | poolData: Record
32 | ): Promise> {
33 | let tokens: TokenE[] = [];
34 |
35 | Object.values(poolData).map(async (pool) => {
36 | tokens.push(...pool.getTokenList());
37 | });
38 |
39 | tokens = Array.from(new Set(tokens));
40 |
41 | let tokenBalances: Record = {};
42 |
43 | let promises = tokens.map(async (token) => {
44 | tokenBalances[token] = await fetchTokenBalance(
45 | token,
46 | publicKey,
47 | connection
48 | );
49 | });
50 | await Promise.all(promises);
51 |
52 | return tokenBalances;
53 | }
54 |
55 | export async function getUserTokenSingle() {}
56 |
57 | export async function getAllUserData(
58 | connection: Connection,
59 | publicKey: PublicKey,
60 | poolData: Record
61 | ): Promise {
62 | let lpBalances = await getUserLpAll(connection, publicKey, poolData);
63 |
64 | let tokenBalances = await getUserTokenAll(connection, publicKey, poolData);
65 |
66 | let userData = new UserAccount(lpBalances, tokenBalances);
67 |
68 | return userData;
69 | }
70 |
--------------------------------------------------------------------------------
/ui/src/hooks/useHydrateStore.ts:
--------------------------------------------------------------------------------
1 | import { fetchAllStats } from "@/hooks/storeHelpers/fetchPrices";
2 | import { getAllUserData } from "@/hooks/storeHelpers/fetchUserData";
3 | import { useGlobalStore } from "@/stores/store";
4 | import { useConnection, useWallet } from "@solana/wallet-adapter-react";
5 | import { useEffect } from "react";
6 | import { getCustodyData } from "./storeHelpers/fetchCustodies";
7 | import { getPoolData } from "./storeHelpers/fetchPools";
8 | import { getPositionData } from "./storeHelpers/fetchPositions";
9 |
10 | export const useHydrateStore = () => {
11 | const setCustodyData = useGlobalStore((state) => state.setCustodyData);
12 | const setPoolData = useGlobalStore((state) => state.setPoolData);
13 | const setPositionData = useGlobalStore((state) => state.setPositionData);
14 |
15 | const poolData = useGlobalStore((state) => state.poolData);
16 |
17 | const setUserData = useGlobalStore((state) => state.setUserData);
18 | const setPriceStats = useGlobalStore((state) => state.setPriceStats);
19 |
20 | const { connection } = useConnection();
21 | const { publicKey } = useWallet();
22 |
23 | useEffect(() => {
24 | (async () => {
25 | const custodyData = await getCustodyData();
26 | const poolData = await getPoolData(custodyData);
27 | const positionInfos = await getPositionData(custodyData);
28 |
29 | setCustodyData(custodyData);
30 | setPoolData(poolData);
31 | setPositionData(positionInfos);
32 | })();
33 | }, []);
34 |
35 | useEffect(() => {
36 | if (
37 | publicKey &&
38 | Object.values(poolData).length > 0
39 | // && Object.values(userData.lpBalances).length == 0
40 | ) {
41 | (async () => {
42 | const userData = await getAllUserData(connection, publicKey, poolData);
43 | setUserData(userData);
44 | })();
45 | }
46 | }, [publicKey, poolData]);
47 |
48 | useEffect(() => {
49 | const fetchAndSetStats = async () => {
50 | const priceStats = await fetchAllStats();
51 | setPriceStats(priceStats);
52 | };
53 |
54 | fetchAndSetStats();
55 |
56 | const interval = setInterval(fetchAndSetStats, 30000);
57 |
58 | return () => clearInterval(interval);
59 | }, []);
60 | };
61 |
--------------------------------------------------------------------------------
/ui/src/components/PoolModal/LpSelector.tsx:
--------------------------------------------------------------------------------
1 | import { MaxButton } from "@/components/Atoms/MaxButton";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | interface Props {
5 | className?: string;
6 | label?: string;
7 | amount: number;
8 | onChangeAmount?(amount: number): void;
9 | maxBalance?: number;
10 | pendingRateConversion?: boolean;
11 | }
12 |
13 | export const LpSelector = (props: Props) => {
14 | return (
15 |
16 |
29 |
30 |
{props.label ? props.label : "LP Tokens"}
31 |
32 |
36 |
37 |
38 | {props.pendingRateConversion ? (
39 |
Loading...
40 | ) : (
41 |
{
62 | const value = e.currentTarget.valueAsNumber;
63 | props.onChangeAmount?.(isNaN(value) ? 0 : value);
64 | }}
65 | />
66 | )}
67 |
68 |
69 |
70 | );
71 | };
72 |
--------------------------------------------------------------------------------
/ui/src/components/Positions/ExistingPositions.tsx:
--------------------------------------------------------------------------------
1 | import { NoPositions } from "@/components/Positions/NoPositions";
2 | import PoolPositionHeader from "@/components/Positions/PoolPositionHeader";
3 | import PoolPositionRow from "@/components/Positions/PoolPositionRow";
4 | import { useGlobalStore } from "@/stores/store";
5 | import { countDictList, getPoolSortedPositions } from "@/utils/organizers";
6 | import { useWallet } from "@solana/wallet-adapter-react";
7 | import { twMerge } from "tailwind-merge";
8 |
9 | interface Props {
10 | className?: string;
11 | }
12 |
13 | export function ExistingPositions(props: Props) {
14 | const { publicKey } = useWallet();
15 |
16 | const positionData = useGlobalStore((state) => state.positionData);
17 |
18 | let positions;
19 |
20 | if (publicKey) {
21 | positions = getPoolSortedPositions(positionData, publicKey);
22 | } else {
23 | positions = getPoolSortedPositions(positionData);
24 | }
25 |
26 | if (countDictList(positions) === 0) {
27 | return ;
28 | }
29 |
30 | return (
31 | <>
32 | {Object.entries(positions).map(([pool, positions]) => (
33 |
34 |
test
35 |
45 | {/* We cannot use a real grid layout here since we have nested grids.
46 | Instead, we're going to fake a grid by assinging column widths to
47 | percentages. */}
48 |
49 |
50 | {positions.map((position, index) => (
51 | // eslint-disable-next-line react/jsx-no-undef
52 |
60 | ))}
61 |
62 | ))}
63 | >
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "perps-ui",
3 | "version": "1.0",
4 | "scripts": {
5 | "dev": "next dev",
6 | "build": "next build",
7 | "start": "next start"
8 | },
9 | "dependencies": {
10 | "@carbon/icons-react": "^11.14.0",
11 | "@coral-xyz/borsh": "^0.26.0",
12 | "@metaplex-foundation/js": "^0.17.12",
13 | "@project-serum/anchor": "^0.25.0-beta.1",
14 | "@pythnetwork/client": "^2.15.0",
15 | "@radix-ui/react-dialog": "^1.0.3",
16 | "@radix-ui/react-dropdown-menu": "^2.0.2",
17 | "@radix-ui/react-slider": "^1.1.0",
18 | "@solana/spl-token": "^0.2.0",
19 | "@solana/wallet-adapter-base": "^0.9.20",
20 | "@solana/wallet-adapter-react": "^0.15.26",
21 | "@solana/wallet-adapter-react-ui": "^0.9.24",
22 | "@solana/wallet-adapter-wallets": "^0.19.9",
23 | "@solana/web3.js": "^1.73.0",
24 | "@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
25 | "axios": "^1.2.2",
26 | "bs58": "^5.0.0",
27 | "date-fns": "^2.29.3",
28 | "next": "^13.1.2",
29 | "postcss-import": "^15.1.0",
30 | "prettier-plugin-tailwindcss": "^0.2.1",
31 | "react": "^18.1.0",
32 | "react-dom": "^18.1.0",
33 | "react-icons": "^4.4.0",
34 | "react-toastify": "^9.1.1",
35 | "react-tradingview-widget": "^1.3.2",
36 | "tailwind-merge": "^1.8.1",
37 | "zustand": "^4.3.3"
38 | },
39 | "devDependencies": {
40 | "@types/carbon__icons-react": "^11.12.0",
41 | "@types/node": "^17.0.38",
42 | "@types/react": "^18.0.10",
43 | "@typescript-eslint/eslint-plugin": "^5.27.0",
44 | "@typescript-eslint/parser": "^5.27.0",
45 | "autoprefixer": "^10.4.7",
46 | "cssnano": "^5.1.10",
47 | "eslint": "^8.16.0",
48 | "eslint-config-next": "^12.1.6",
49 | "eslint-config-prettier": "^8.5.0",
50 | "eslint-plugin-import": "^2.26.0",
51 | "eslint-plugin-prettier": "^4.0.0",
52 | "eslint-plugin-react": "^7.30.0",
53 | "eslint-plugin-react-hooks": "^4.5.0",
54 | "eslint-plugin-simple-import-sort": "^7.0.0",
55 | "eslint-plugin-tailwindcss": "^3.5.0",
56 | "eslint-plugin-testing-library": "^5.5.1",
57 | "eslint-plugin-unused-imports": "^2.0.0",
58 | "postcss": "^8.4.14",
59 | "prettier": "^2.6.2",
60 | "tailwindcss": "^3.0.24",
61 | "typescript": "^4.7.2"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/ui/src/components/Chart/ChartCurrency.tsx:
--------------------------------------------------------------------------------
1 | import ChevronDownIcon from "@carbon/icons-react/lib/ChevronDown";
2 | import { useRouter } from "next/router";
3 | import { cloneElement, useState } from "react";
4 | import { twMerge } from "tailwind-merge";
5 |
6 | import { TokenE, getTokenIcon, getTokenLabel } from "@/lib/Token";
7 |
8 | import { TokenSelectorList } from "../TokenSelectorList";
9 |
10 | interface Props {
11 | className?: string;
12 | comparisonCurrency: "usd" | "eur" | TokenE.USDC | TokenE.USDT;
13 | token: TokenE;
14 | }
15 |
16 | export function ChartCurrency(props: Props) {
17 | const tokenIcon = getTokenIcon(props.token);
18 | const [selectorOpen, setSelectorOpen] = useState(false);
19 | const router = useRouter();
20 |
21 | return (
22 | <>
23 | setSelectorOpen((cur) => !cur)}
32 | >
33 | {cloneElement(tokenIcon, {
34 | className: twMerge(tokenIcon.props.className, "h-8", "w-8"),
35 | })}
36 |
37 |
{props.token}
38 |
39 | {getTokenLabel(props.token)}
40 |
41 |
42 |
59 |
60 | {selectorOpen && (
61 | setSelectorOpen(false)}
63 | onSelectToken={(token) => {
64 | router.push(`/trade/${token}-usd`);
65 | }}
66 | />
67 | )}
68 | >
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/DISCLAIMER.md:
--------------------------------------------------------------------------------
1 | # Disclaimer
2 |
3 | All claims, content, designs, algorithms, estimates, roadmaps, specifications, and performance measurements described in this project are done with the good faith efforts Solana Labs, Inc. and its affiliates ("SL"). It is up to the reader to check and validate their accuracy and truthfulness. Furthermore nothing in this project constitutes a solicitation for investment.
4 | Any content produced by SL or developer resources that SL provides have not been subject to audit and are for educational and inspiration purposes only. SL does not encourage, induce or sanction the deployment, integration or use of any such applications (including the code comprising the Solana blockchain protocol) in violation of applicable laws or regulations and hereby prohibits any such deployment, integration or use. This includes use of any such applications by the reader (a) in violation of export control or sanctions laws of the United States or any other applicable jurisdiction, (b) if the reader is located in or ordinarily resident in a country or territory subject to comprehensive sanctions administered by the U.S. Office of Foreign Assets Control (OFAC), or (c) if the reader is or is working on behalf of a Specially Designated National (SDN) or a person subject to similar blocking or denied party prohibitions.
5 | The reader should be aware that U.S. export control and sanctions laws prohibit U.S. persons (and other persons that are subject to such laws) from transacting with persons in certain countries and territories or that are on the SDN list. As a project based primarily on open-source software, it is possible that such sanctioned persons may nevertheless bypass prohibitions, obtain the code comprising the Solana blockchain protocol (or other project code or applications) and deploy, integrate, or otherwise use it. Accordingly, there is a risk to individuals that other persons using the Solana blockchain protocol may be sanctioned persons and that transactions with such persons would be a violation of U.S. export controls and sanctions law. This risk applies to individuals, organizations, and other ecosystem participants that deploy, integrate, or use the Solana blockchain protocol code directly (e.g., as a node operator), and individuals that transact on the Solana blockchain through light clients, third party interfaces, and/or wallet software.
6 |
--------------------------------------------------------------------------------
/programs/perpetuals/src/instructions/get_liquidation_state.rs:
--------------------------------------------------------------------------------
1 | //! GetLiquidationState instruction handler
2 |
3 | use {
4 | crate::state::{
5 | custody::Custody, oracle::OraclePrice, perpetuals::Perpetuals, pool::Pool,
6 | position::Position,
7 | },
8 | anchor_lang::prelude::*,
9 | };
10 |
11 | #[derive(Accounts)]
12 | pub struct GetLiquidationState<'info> {
13 | #[account(
14 | seeds = [b"perpetuals"],
15 | bump = perpetuals.perpetuals_bump
16 | )]
17 | pub perpetuals: Box>,
18 |
19 | #[account(
20 | seeds = [b"pool",
21 | pool.name.as_bytes()],
22 | bump = pool.bump
23 | )]
24 | pub pool: Box>,
25 |
26 | #[account(
27 | seeds = [b"position",
28 | position.owner.as_ref(),
29 | pool.key().as_ref(),
30 | custody.key().as_ref(),
31 | &[position.side as u8]],
32 | bump = position.bump
33 | )]
34 | pub position: Box>,
35 |
36 | #[account(
37 | seeds = [b"custody",
38 | pool.key().as_ref(),
39 | custody.mint.as_ref()],
40 | bump = custody.bump
41 | )]
42 | pub custody: Box>,
43 |
44 | /// CHECK: oracle account for the collateral token
45 | #[account(
46 | constraint = custody_oracle_account.key() == custody.oracle.oracle_account
47 | )]
48 | pub custody_oracle_account: AccountInfo<'info>,
49 | }
50 |
51 | #[derive(AnchorSerialize, AnchorDeserialize)]
52 | pub struct GetLiquidationStateParams {}
53 |
54 | pub fn get_liquidation_state(
55 | ctx: Context,
56 | _params: &GetLiquidationStateParams,
57 | ) -> Result {
58 | let custody = ctx.accounts.custody.as_mut();
59 | let curtime = ctx.accounts.perpetuals.get_time()?;
60 |
61 | let token_ema_price = OraclePrice::new_from_oracle(
62 | custody.oracle.oracle_type,
63 | &ctx.accounts.custody_oracle_account.to_account_info(),
64 | custody.oracle.max_price_error,
65 | custody.oracle.max_price_age_sec,
66 | curtime,
67 | custody.pricing.use_ema,
68 | )?;
69 |
70 | if ctx.accounts.pool.check_leverage(
71 | &ctx.accounts.position,
72 | &token_ema_price,
73 | custody,
74 | curtime,
75 | false,
76 | )? {
77 | Ok(0)
78 | } else {
79 | Ok(1)
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/ui/src/hooks/storeHelpers/fetchPools.ts:
--------------------------------------------------------------------------------
1 | import { CustodyAccount } from "@/lib/CustodyAccount";
2 | import { PoolAccount } from "@/lib/PoolAccount";
3 | import { Pool } from "@/lib/types";
4 | import { getPerpetualProgramAndProvider } from "@/utils/constants";
5 | import { ViewHelper } from "@/utils/viewHelpers";
6 | import { findProgramAddressSync } from "@project-serum/anchor/dist/cjs/utils/pubkey";
7 | import { getMint } from "@solana/spl-token";
8 | import { PublicKey } from "@solana/web3.js";
9 |
10 | interface FetchPool {
11 | account: Pool;
12 | publicKey: PublicKey;
13 | }
14 |
15 | export async function getPoolData(
16 | custodyInfos: Record
17 | ): Promise> {
18 | let { perpetual_program, provider } = await getPerpetualProgramAndProvider();
19 |
20 | // @ts-ignore
21 | let fetchedPools: FetchPool[] = await perpetual_program.account.pool.all();
22 | let poolObjs: Record = {};
23 |
24 | await Promise.all(
25 | Object.values(fetchedPools)
26 | .sort((a, b) => a.account.name.localeCompare(b.account.name))
27 | .map(async (pool: FetchPool) => {
28 | let lpTokenMint = findProgramAddressSync(
29 | [Buffer.from("lp_token_mint"), pool.publicKey.toBuffer()],
30 | perpetual_program.programId
31 | )[0];
32 |
33 | const lpData = await getMint(provider.connection, lpTokenMint);
34 |
35 | const View = new ViewHelper(provider.connection, provider);
36 |
37 | console.log("fetching pools actual data", pool);
38 |
39 | let poolData: Pool = {
40 | name: pool.account.name,
41 | custodies: pool.account.custodies,
42 | ratios: pool.account.ratios,
43 | aumUsd: pool.account.aumUsd,
44 | bump: pool.account.bump,
45 | lpTokenBump: pool.account.lpTokenBump,
46 | inceptionTime: pool.account.inceptionTime,
47 | };
48 |
49 | poolObjs[pool.publicKey.toString()] = new PoolAccount(
50 | poolData,
51 | custodyInfos,
52 | pool.publicKey,
53 | lpData
54 | );
55 | let fetchedAum;
56 |
57 | let loopStatus = true;
58 |
59 | while (loopStatus) {
60 | try {
61 | fetchedAum = await View.getAssetsUnderManagement(
62 | poolObjs[pool.publicKey.toString()]
63 | );
64 | loopStatus = false;
65 | } catch (error) {}
66 | }
67 |
68 | poolObjs[pool.publicKey.toString()].setAum(fetchedAum);
69 | })
70 | );
71 |
72 | return poolObjs;
73 | }
74 |
--------------------------------------------------------------------------------
/programs/perpetuals/tests/native/utils/pda.rs:
--------------------------------------------------------------------------------
1 | use {perpetuals::state::position::Side, solana_sdk::pubkey::Pubkey};
2 |
3 | pub fn get_multisig_pda() -> (Pubkey, u8) {
4 | Pubkey::find_program_address(&["multisig".as_ref()], &perpetuals::id())
5 | }
6 |
7 | pub fn get_transfer_authority_pda() -> (Pubkey, u8) {
8 | Pubkey::find_program_address(&["transfer_authority".as_ref()], &perpetuals::id())
9 | }
10 |
11 | pub fn get_perpetuals_pda() -> (Pubkey, u8) {
12 | Pubkey::find_program_address(&["perpetuals".as_ref()], &perpetuals::id())
13 | }
14 |
15 | pub fn get_program_data_pda() -> (Pubkey, u8) {
16 | Pubkey::find_program_address(
17 | &[perpetuals::id().as_ref()],
18 | &solana_program::bpf_loader_upgradeable::id(),
19 | )
20 | }
21 |
22 | pub fn get_pool_pda(name: String) -> (Pubkey, u8) {
23 | Pubkey::find_program_address(&["pool".as_ref(), name.as_bytes()], &perpetuals::id())
24 | }
25 |
26 | pub fn get_lp_token_mint_pda(pool_pda: &Pubkey) -> (Pubkey, u8) {
27 | Pubkey::find_program_address(
28 | &["lp_token_mint".as_ref(), pool_pda.as_ref()],
29 | &perpetuals::id(),
30 | )
31 | }
32 |
33 | pub fn get_custody_pda(pool_pda: &Pubkey, custody_token_mint: &Pubkey) -> (Pubkey, u8) {
34 | Pubkey::find_program_address(
35 | &[
36 | "custody".as_ref(),
37 | pool_pda.as_ref(),
38 | custody_token_mint.as_ref(),
39 | ],
40 | &perpetuals::id(),
41 | )
42 | }
43 |
44 | pub fn get_position_pda(
45 | owner: &Pubkey,
46 | pool_pda: &Pubkey,
47 | custody_pda: &Pubkey,
48 | side: Side,
49 | ) -> (Pubkey, u8) {
50 | Pubkey::find_program_address(
51 | &[
52 | "position".as_ref(),
53 | owner.as_ref(),
54 | pool_pda.as_ref(),
55 | custody_pda.as_ref(),
56 | &[side as u8],
57 | ],
58 | &perpetuals::id(),
59 | )
60 | }
61 |
62 | pub fn get_custody_token_account_pda(
63 | pool_pda: &Pubkey,
64 | custody_token_mint: &Pubkey,
65 | ) -> (Pubkey, u8) {
66 | Pubkey::find_program_address(
67 | &[
68 | "custody_token_account".as_ref(),
69 | pool_pda.as_ref(),
70 | custody_token_mint.as_ref(),
71 | ],
72 | &perpetuals::id(),
73 | )
74 | }
75 |
76 | pub fn get_test_oracle_account(pool_pda: &Pubkey, custody_mint: &Pubkey) -> (Pubkey, u8) {
77 | Pubkey::find_program_address(
78 | &[
79 | "oracle_account".as_ref(),
80 | pool_pda.as_ref(),
81 | custody_mint.as_ref(),
82 | ],
83 | &perpetuals::id(),
84 | )
85 | }
86 |
--------------------------------------------------------------------------------
/ui/src/utils/retrieveData.ts:
--------------------------------------------------------------------------------
1 | import { PoolAccount } from "@/lib/PoolAccount";
2 | import { getTokenAddress, TokenE } from "@/lib/Token";
3 | import { getAssociatedTokenAddress, Mint } from "@solana/spl-token";
4 | import { Connection, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
5 |
6 | export async function checkIfAccountExists(
7 | account: PublicKey,
8 | connection: Connection
9 | ): Promise {
10 | let bal = await connection.getBalance(account);
11 | if (bal > 0) {
12 | return true;
13 | } else {
14 | return false;
15 | }
16 | }
17 |
18 | export async function fetchTokenBalance(
19 | payToken: TokenE,
20 | publicKey: PublicKey,
21 | connection: Connection
22 | ): Promise {
23 | console.log("fetching user token", payToken);
24 | let tokenATA = await getAssociatedTokenAddress(
25 | new PublicKey(getTokenAddress(payToken)),
26 | publicKey
27 | );
28 | let balance = 0;
29 |
30 | if (await checkIfAccountExists(tokenATA, connection)) {
31 | balance = (await connection.getTokenAccountBalance(tokenATA)).value
32 | .uiAmount!;
33 | }
34 |
35 | if (payToken === TokenE.SOL) {
36 | let solBalance =
37 | (await connection.getBalance(publicKey)) / LAMPORTS_PER_SOL;
38 | console.log("sol balance", solBalance, "token balance", balance);
39 | return balance + solBalance;
40 | }
41 | return balance;
42 | }
43 |
44 | export async function fetchLPBalance(
45 | address: PublicKey,
46 | publicKey: PublicKey,
47 | connection: Connection
48 | ): Promise {
49 | let lpTokenAccount = await getAssociatedTokenAddress(address, publicKey);
50 | if (!(await checkIfAccountExists(lpTokenAccount, connection))) {
51 | return 0;
52 | } else {
53 | let balance = await connection.getTokenAccountBalance(lpTokenAccount);
54 | return balance.value.uiAmount!;
55 | }
56 | }
57 |
58 | export function getLiquidityBalance(
59 | pool: PoolAccount,
60 | userLpBalance: number,
61 | stats: Record
62 | ): number {
63 | let lpSupply = Number(pool.lpData.supply) / 10 ** pool.lpData.decimals;
64 | let userLiquidity = (userLpBalance! / lpSupply) * pool.getLiquidities(stats)!;
65 |
66 | if (Number.isNaN(userLiquidity)) {
67 | return 0;
68 | }
69 |
70 | return userLiquidity;
71 | }
72 |
73 | export function getLiquidityShare(
74 | pool: PoolAccount,
75 | userLpBalance: number
76 | ): number {
77 | let lpSupply = Number(pool.lpData.supply) / 10 ** pool.lpData.decimals;
78 |
79 | let userShare = (userLpBalance! / lpSupply) * 100;
80 |
81 | if (Number.isNaN(userShare)) {
82 | return 0;
83 | }
84 | return userShare;
85 | }
86 |
--------------------------------------------------------------------------------
/ui/src/components/Positions/PoolPositionRow.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingDots } from "@/components/LoadingDots";
2 | import { PositionAdditionalInfo } from "@/components/Positions/PositionAdditionalInfo";
3 | import PositionBasicInfo from "@/components/Positions/PositionBasicInfo";
4 | import { PositionAccount } from "@/lib/PositionAccount";
5 | import { useGlobalStore } from "@/stores/store";
6 | import { getPerpetualProgramAndProvider } from "@/utils/constants";
7 | import { ViewHelper } from "@/utils/viewHelpers";
8 | import { useConnection, useWallet } from "@solana/wallet-adapter-react";
9 | import { useEffect, useState } from "react";
10 | import { twMerge } from "tailwind-merge";
11 |
12 | interface Props {
13 | className?: string;
14 | position: PositionAccount;
15 | }
16 |
17 | export default function PoolPositionRow(props: Props) {
18 | const { connection } = useConnection();
19 | const { wallet } = useWallet();
20 |
21 | const poolData = useGlobalStore((state) => state.poolData);
22 |
23 | const [expanded, setExpanded] = useState(false);
24 |
25 | const [pnl, setPnl] = useState(0);
26 | const [liqPrice, setLiqPrice] = useState(0);
27 |
28 | useEffect(() => {
29 | async function fetchData() {
30 | let { provider } = await getPerpetualProgramAndProvider(wallet as any);
31 |
32 | const View = new ViewHelper(connection, provider);
33 |
34 | let fetchedPnlPrice = await View.getPnl(props.position);
35 |
36 | let finalPnl = Number(fetchedPnlPrice.profit)
37 | ? Number(fetchedPnlPrice.profit)
38 | : -1 * Number(fetchedPnlPrice.loss);
39 | setPnl(finalPnl / 10 ** 6);
40 |
41 | let fetchedLiqPrice = await View.getLiquidationPrice(props.position);
42 |
43 | setLiqPrice(Number(fetchedLiqPrice) / 10 ** 6);
44 | }
45 | if (Object.keys(poolData).length > 0) {
46 | fetchData();
47 | }
48 | }, [poolData]);
49 |
50 | if (pnl === null) {
51 | return ;
52 | }
53 |
54 | return (
55 |
56 |
setExpanded((cur) => !cur)}
63 | />
64 |
75 |
76 | );
77 | }
78 |
--------------------------------------------------------------------------------
/ui/src/components/TradeSidebar/TradeDetails.tsx:
--------------------------------------------------------------------------------
1 | import { getTokenIcon, TokenE } from "@/lib/Token";
2 | import { Side } from "@/lib/types";
3 | import {
4 | formatFees,
5 | formatNumber,
6 | formatNumberLessThan,
7 | formatPrice,
8 | } from "@/utils/formatters";
9 | import { cloneElement } from "react";
10 | import { twMerge } from "tailwind-merge";
11 |
12 | interface Props {
13 | className?: string;
14 | collateralToken: TokenE;
15 | positionToken: TokenE;
16 | entryPrice: number;
17 | liquidationPrice: number;
18 | fees: number;
19 | availableLiquidity: number;
20 | borrowRate: number;
21 | side: Side;
22 | onSubmit?(): void;
23 | }
24 |
25 | export function TradeDetails(props: Props) {
26 | const icon = getTokenIcon(props.positionToken);
27 |
28 | return (
29 |
30 |
39 |
40 | {[
41 | {
42 | label: "Collateral in",
43 | value: "USD",
44 | },
45 | {
46 | label: "Entry Price",
47 | value: `$${formatNumber(props.entryPrice)}`,
48 | },
49 | {
50 | label: "Liq. Price",
51 | value: `$${formatNumber(props.liquidationPrice)}`,
52 | },
53 | {
54 | label: "Fees",
55 | value: `${formatNumberLessThan(props.fees)}`,
56 | },
57 | {
58 | label: "Borrow Rate",
59 | value: (
60 | <>
61 | {`${formatFees(100 * props.borrowRate)}% / hr`}
62 |
63 | >
64 | ),
65 | },
66 | {
67 | label: "Available Liquidity",
68 | value: `$${formatPrice(props.availableLiquidity)}`,
69 | },
70 | ].map(({ label, value }, i) => (
71 |
79 |
{label}
80 |
{value}
81 |
82 | ))}
83 |
84 |
85 | );
86 | }
87 |
--------------------------------------------------------------------------------
/programs/perpetuals/tests/native/instructions/test_set_custody_config.rs:
--------------------------------------------------------------------------------
1 | use {
2 | crate::utils::{self, pda},
3 | anchor_lang::{
4 | prelude::{AccountMeta, Pubkey},
5 | ToAccountMetas,
6 | },
7 | perpetuals::{
8 | instructions::SetCustodyConfigParams,
9 | state::{custody::Custody, multisig::Multisig},
10 | },
11 | solana_program_test::{BanksClientError, ProgramTestContext},
12 | solana_sdk::signer::{keypair::Keypair, Signer},
13 | };
14 |
15 | pub async fn test_set_custody_config(
16 | program_test_ctx: &mut ProgramTestContext,
17 | admin: &Keypair,
18 | payer: &Keypair,
19 | pool_pda: &Pubkey,
20 | custody_pda: &Pubkey,
21 | params: SetCustodyConfigParams,
22 | multisig_signers: &[&Keypair],
23 | ) -> std::result::Result<(), BanksClientError> {
24 | // ==== WHEN ==============================================================
25 | let multisig_pda = pda::get_multisig_pda().0;
26 | let multisig_account = utils::get_account::(program_test_ctx, multisig_pda).await;
27 |
28 | // One Tx per multisig signer
29 | for i in 0..multisig_account.min_signatures {
30 | let signer: &Keypair = multisig_signers[i as usize];
31 |
32 | let accounts_meta = {
33 | let accounts = perpetuals::accounts::SetCustodyConfig {
34 | admin: admin.pubkey(),
35 | multisig: multisig_pda,
36 | pool: *pool_pda,
37 | custody: *custody_pda,
38 | };
39 |
40 | let mut accounts_meta = accounts.to_account_metas(None);
41 |
42 | accounts_meta.push(AccountMeta {
43 | pubkey: signer.pubkey(),
44 | is_signer: true,
45 | is_writable: false,
46 | });
47 |
48 | accounts_meta
49 | };
50 |
51 | utils::create_and_execute_perpetuals_ix(
52 | program_test_ctx,
53 | accounts_meta,
54 | perpetuals::instruction::SetCustodyConfig {
55 | params: params.clone(),
56 | },
57 | Some(&payer.pubkey()),
58 | &[admin, payer, signer],
59 | )
60 | .await?;
61 | }
62 |
63 | // ==== THEN ==============================================================
64 | let custody_account = utils::get_account::(program_test_ctx, *custody_pda).await;
65 |
66 | // Check custody account
67 | {
68 | assert_eq!(custody_account.pool, *pool_pda);
69 | assert_eq!(custody_account.is_stable, params.is_stable);
70 | assert_eq!(custody_account.oracle, params.oracle);
71 | assert_eq!(custody_account.pricing, params.pricing);
72 | assert_eq!(custody_account.permissions, params.permissions);
73 | assert_eq!(custody_account.fees, params.fees);
74 | }
75 |
76 | Ok(())
77 | }
78 |
--------------------------------------------------------------------------------
/programs/perpetuals/tests/native/utils/fixtures.rs:
--------------------------------------------------------------------------------
1 | // Contains fixtures values usable in tests, made to reduce boilerplate
2 |
3 | use {
4 | anchor_lang::prelude::Pubkey,
5 | perpetuals::{
6 | instructions::InitParams,
7 | state::{
8 | custody::{BorrowRateParams, Fees, FeesMode, OracleParams, PricingParams},
9 | oracle::OracleType,
10 | perpetuals::Permissions,
11 | },
12 | },
13 | };
14 |
15 | pub fn permissions_full() -> Permissions {
16 | Permissions {
17 | allow_swap: true,
18 | allow_add_liquidity: true,
19 | allow_remove_liquidity: true,
20 | allow_open_position: true,
21 | allow_close_position: true,
22 | allow_pnl_withdrawal: true,
23 | allow_collateral_withdrawal: true,
24 | allow_size_change: true,
25 | }
26 | }
27 |
28 | pub fn borrow_rate_regular() -> BorrowRateParams {
29 | BorrowRateParams {
30 | base_rate: 0,
31 | slope1: 80_000,
32 | slope2: 120_000,
33 | optimal_utilization: 800_000_000,
34 | }
35 | }
36 |
37 | pub fn fees_linear_regular() -> Fees {
38 | Fees {
39 | mode: FeesMode::Linear,
40 | ratio_mult: 20_000,
41 | utilization_mult: 20_000,
42 | swap_in: 100,
43 | swap_out: 100,
44 | stable_swap_in: 100,
45 | stable_swap_out: 100,
46 | add_liquidity: 200,
47 | remove_liquidity: 300,
48 | open_position: 100,
49 | close_position: 100,
50 | liquidation: 50,
51 | protocol_share: 25,
52 | }
53 | }
54 |
55 | pub fn pricing_params_regular(use_ema: bool) -> PricingParams {
56 | PricingParams {
57 | use_ema,
58 | use_unrealized_pnl_in_aum: true,
59 | trade_spread_long: 100,
60 | trade_spread_short: 100,
61 | swap_spread: 300,
62 | min_initial_leverage: 10_000,
63 | max_initial_leverage: 100_000,
64 | max_leverage: 100_000,
65 | max_payoff_mult: 10_000,
66 | max_utilization: 0,
67 | max_position_locked_usd: 0,
68 | max_total_locked_usd: 0,
69 | }
70 | }
71 |
72 | pub fn oracle_params_regular(oracle_account: Pubkey) -> OracleParams {
73 | OracleParams {
74 | oracle_account,
75 | oracle_type: OracleType::Test,
76 | max_price_error: 1_000_000,
77 | max_price_age_sec: 30,
78 | }
79 | }
80 |
81 | pub fn init_params_permissions_full(min_signatures: u8) -> InitParams {
82 | InitParams {
83 | min_signatures,
84 | allow_swap: true,
85 | allow_add_liquidity: true,
86 | allow_remove_liquidity: true,
87 | allow_open_position: true,
88 | allow_close_position: true,
89 | allow_pnl_withdrawal: true,
90 | allow_collateral_withdrawal: true,
91 | allow_size_change: true,
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/programs/perpetuals/src/instructions/remove_pool.rs:
--------------------------------------------------------------------------------
1 | //! RemovePool instruction handler
2 |
3 | use {
4 | crate::{
5 | error::PerpetualsError,
6 | state::{
7 | multisig::{AdminInstruction, Multisig},
8 | perpetuals::Perpetuals,
9 | pool::Pool,
10 | },
11 | },
12 | anchor_lang::prelude::*,
13 | };
14 |
15 | #[derive(Accounts)]
16 | pub struct RemovePool<'info> {
17 | #[account(mut)]
18 | pub admin: Signer<'info>,
19 |
20 | #[account(
21 | mut,
22 | seeds = [b"multisig"],
23 | bump = multisig.load()?.bump
24 | )]
25 | pub multisig: AccountLoader<'info, Multisig>,
26 |
27 | /// CHECK: empty PDA, authority for token accounts
28 | #[account(
29 | mut,
30 | seeds = [b"transfer_authority"],
31 | bump = perpetuals.transfer_authority_bump
32 | )]
33 | pub transfer_authority: AccountInfo<'info>,
34 |
35 | #[account(
36 | mut,
37 | realloc = Perpetuals::LEN + (perpetuals.pools.len() - 1) * 32,
38 | realloc::payer = admin,
39 | realloc::zero = false,
40 | seeds = [b"perpetuals"],
41 | bump = perpetuals.perpetuals_bump
42 | )]
43 | pub perpetuals: Box>,
44 |
45 | #[account(
46 | mut,
47 | seeds = [b"pool",
48 | pool.name.as_bytes()],
49 | bump = pool.bump,
50 | close = transfer_authority
51 | )]
52 | pub pool: Box>,
53 |
54 | system_program: Program<'info, System>,
55 | }
56 |
57 | #[derive(AnchorSerialize, AnchorDeserialize)]
58 | pub struct RemovePoolParams {}
59 |
60 | pub fn remove_pool<'info>(
61 | ctx: Context<'_, '_, '_, 'info, RemovePool<'info>>,
62 | params: &RemovePoolParams,
63 | ) -> Result {
64 | // validate signatures
65 | let mut multisig = ctx.accounts.multisig.load_mut()?;
66 |
67 | let signatures_left = multisig.sign_multisig(
68 | &ctx.accounts.admin,
69 | &Multisig::get_account_infos(&ctx)[1..],
70 | &Multisig::get_instruction_data(AdminInstruction::RemovePool, params)?,
71 | )?;
72 | if signatures_left > 0 {
73 | msg!(
74 | "Instruction has been signed but more signatures are required: {}",
75 | signatures_left
76 | );
77 | return Ok(signatures_left);
78 | }
79 |
80 | require!(
81 | ctx.accounts.pool.custodies.is_empty(),
82 | PerpetualsError::InvalidPoolState
83 | );
84 |
85 | // remove pool from the list
86 | let perpetuals = ctx.accounts.perpetuals.as_mut();
87 | let pool_idx = perpetuals
88 | .pools
89 | .iter()
90 | .position(|x| *x == ctx.accounts.pool.key())
91 | .ok_or(PerpetualsError::InvalidPoolState)?;
92 | perpetuals.pools.remove(pool_idx);
93 |
94 | Ok(0)
95 | }
96 |
--------------------------------------------------------------------------------
/programs/perpetuals/src/instructions/set_permissions.rs:
--------------------------------------------------------------------------------
1 | //! SetPermissions instruction handler
2 |
3 | use {
4 | crate::{
5 | error::PerpetualsError,
6 | state::{
7 | multisig::{AdminInstruction, Multisig},
8 | perpetuals::Perpetuals,
9 | },
10 | },
11 | anchor_lang::prelude::*,
12 | };
13 |
14 | #[derive(Accounts)]
15 | pub struct SetPermissions<'info> {
16 | #[account()]
17 | pub admin: Signer<'info>,
18 |
19 | #[account(
20 | mut,
21 | seeds = [b"multisig"],
22 | bump = multisig.load()?.bump
23 | )]
24 | pub multisig: AccountLoader<'info, Multisig>,
25 |
26 | #[account(
27 | mut,
28 | seeds = [b"perpetuals"],
29 | bump = perpetuals.perpetuals_bump
30 | )]
31 | pub perpetuals: Box>,
32 | }
33 |
34 | #[derive(AnchorSerialize, AnchorDeserialize)]
35 | pub struct SetPermissionsParams {
36 | pub allow_swap: bool,
37 | pub allow_add_liquidity: bool,
38 | pub allow_remove_liquidity: bool,
39 | pub allow_open_position: bool,
40 | pub allow_close_position: bool,
41 | pub allow_pnl_withdrawal: bool,
42 | pub allow_collateral_withdrawal: bool,
43 | pub allow_size_change: bool,
44 | }
45 |
46 | pub fn set_permissions<'info>(
47 | ctx: Context<'_, '_, '_, 'info, SetPermissions<'info>>,
48 | params: &SetPermissionsParams,
49 | ) -> Result {
50 | // validate signatures
51 | let mut multisig = ctx.accounts.multisig.load_mut()?;
52 |
53 | let signatures_left = multisig.sign_multisig(
54 | &ctx.accounts.admin,
55 | &Multisig::get_account_infos(&ctx)[1..],
56 | &Multisig::get_instruction_data(AdminInstruction::SetPermissions, params)?,
57 | )?;
58 | if signatures_left > 0 {
59 | msg!(
60 | "Instruction has been signed but more signatures are required: {}",
61 | signatures_left
62 | );
63 | return Ok(signatures_left);
64 | }
65 |
66 | // update permissions
67 | let perpetuals = ctx.accounts.perpetuals.as_mut();
68 | perpetuals.permissions.allow_swap = params.allow_swap;
69 | perpetuals.permissions.allow_add_liquidity = params.allow_add_liquidity;
70 | perpetuals.permissions.allow_remove_liquidity = params.allow_remove_liquidity;
71 | perpetuals.permissions.allow_open_position = params.allow_open_position;
72 | perpetuals.permissions.allow_close_position = params.allow_close_position;
73 | perpetuals.permissions.allow_pnl_withdrawal = params.allow_pnl_withdrawal;
74 | perpetuals.permissions.allow_collateral_withdrawal = params.allow_collateral_withdrawal;
75 | perpetuals.permissions.allow_size_change = params.allow_size_change;
76 |
77 | if !perpetuals.validate() {
78 | err!(PerpetualsError::InvalidPerpetualsConfig)
79 | } else {
80 | Ok(0)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/programs/perpetuals/src/instructions/get_pnl.rs:
--------------------------------------------------------------------------------
1 | //! GetPnl instruction handler
2 |
3 | use {
4 | crate::state::{
5 | custody::Custody,
6 | oracle::OraclePrice,
7 | perpetuals::{Perpetuals, ProfitAndLoss},
8 | pool::Pool,
9 | position::Position,
10 | },
11 | anchor_lang::prelude::*,
12 | };
13 |
14 | #[derive(Accounts)]
15 | pub struct GetPnl<'info> {
16 | #[account(
17 | seeds = [b"perpetuals"],
18 | bump = perpetuals.perpetuals_bump
19 | )]
20 | pub perpetuals: Box>,
21 |
22 | #[account(
23 | seeds = [b"pool",
24 | pool.name.as_bytes()],
25 | bump = pool.bump
26 | )]
27 | pub pool: Box>,
28 |
29 | #[account(
30 | seeds = [b"position",
31 | position.owner.as_ref(),
32 | pool.key().as_ref(),
33 | custody.key().as_ref(),
34 | &[position.side as u8]],
35 | bump = position.bump
36 | )]
37 | pub position: Box>,
38 |
39 | #[account(
40 | seeds = [b"custody",
41 | pool.key().as_ref(),
42 | custody.mint.as_ref()],
43 | bump = custody.bump
44 | )]
45 | pub custody: Box>,
46 |
47 | /// CHECK: oracle account for the collateral token
48 | #[account(
49 | constraint = custody_oracle_account.key() == custody.oracle.oracle_account
50 | )]
51 | pub custody_oracle_account: AccountInfo<'info>,
52 | }
53 |
54 | #[derive(AnchorSerialize, AnchorDeserialize)]
55 | pub struct GetPnlParams {}
56 |
57 | pub fn get_pnl(ctx: Context, _params: &GetPnlParams) -> Result {
58 | // get oracle prices
59 | let position = &ctx.accounts.position;
60 | let pool = &ctx.accounts.pool;
61 | let curtime = ctx.accounts.perpetuals.get_time()?;
62 | let custody = ctx.accounts.custody.as_mut();
63 |
64 | let token_price = OraclePrice::new_from_oracle(
65 | custody.oracle.oracle_type,
66 | &ctx.accounts.custody_oracle_account.to_account_info(),
67 | custody.oracle.max_price_error,
68 | custody.oracle.max_price_age_sec,
69 | curtime,
70 | false,
71 | )?;
72 |
73 | let token_ema_price = OraclePrice::new_from_oracle(
74 | custody.oracle.oracle_type,
75 | &ctx.accounts.custody_oracle_account.to_account_info(),
76 | custody.oracle.max_price_error,
77 | custody.oracle.max_price_age_sec,
78 | curtime,
79 | custody.pricing.use_ema,
80 | )?;
81 |
82 | // compute pnl
83 | let (profit, loss, _) = pool.get_pnl_usd(
84 | position,
85 | &token_price,
86 | &token_ema_price,
87 | custody,
88 | curtime,
89 | false,
90 | )?;
91 |
92 | Ok(ProfitAndLoss { profit, loss })
93 | }
94 |
--------------------------------------------------------------------------------
/ui/src/utils/constants.ts:
--------------------------------------------------------------------------------
1 | import * as PerpetualsJson from "@/target/idl/perpetuals.json";
2 | import { IDL as PERPETUALS_IDL, Perpetuals } from "@/target/types/perpetuals";
3 | import { getProvider } from "@/utils/provider";
4 | import { AnchorProvider, Program, Wallet } from "@project-serum/anchor";
5 | import NodeWallet from "@project-serum/anchor/dist/cjs/nodewallet";
6 | import { findProgramAddressSync } from "@project-serum/anchor/dist/cjs/utils/pubkey";
7 | import { WalletContextState } from "@solana/wallet-adapter-react";
8 | import { Keypair, PublicKey, Transaction } from "@solana/web3.js";
9 |
10 | export const PERPETUALS_PROGRAM_ID = new PublicKey(
11 | PerpetualsJson["metadata"]["address"]
12 | );
13 |
14 | class DefaultWallet implements Wallet {
15 | constructor(readonly payer: Keypair) {}
16 |
17 | static local(): NodeWallet | never {
18 | throw new Error("Local wallet not supported");
19 | }
20 |
21 | async signTransaction(tx: Transaction): Promise {
22 | return tx;
23 | }
24 |
25 | async signAllTransactions(txs: Transaction[]): Promise {
26 | return txs;
27 | }
28 |
29 | get publicKey(): PublicKey {
30 | return this.payer.publicKey;
31 | }
32 | }
33 |
34 | export async function getPerpetualProgramAndProvider(
35 | walletContextState?: WalletContextState
36 | ): Promise<{
37 | perpetual_program: Program;
38 | provider: AnchorProvider;
39 | }> {
40 | let provider;
41 |
42 | let perpetual_program;
43 |
44 | if (walletContextState) {
45 | let wallet: Wallet = {
46 | // @ts-ignore
47 | signTransaction: walletContextState.signTransaction,
48 | // @ts-ignore
49 | signAllTransactions: walletContextState.signAllTransactions,
50 | // @ts-ignore
51 | publicKey: walletContextState.publicKey,
52 | };
53 |
54 | provider = await getProvider(wallet);
55 | } else {
56 | provider = await getProvider(new DefaultWallet(DEFAULT_PERPS_USER));
57 | }
58 |
59 | perpetual_program = new Program(
60 | PERPETUALS_IDL,
61 | PERPETUALS_PROGRAM_ID,
62 | provider
63 | );
64 |
65 | return { perpetual_program, provider };
66 | }
67 |
68 | export const TRANSFER_AUTHORITY = findProgramAddressSync(
69 | [Buffer.from("transfer_authority")],
70 | PERPETUALS_PROGRAM_ID
71 | )[0];
72 |
73 | export const PERPETUALS_ADDRESS = findProgramAddressSync(
74 | [Buffer.from("perpetuals")],
75 | PERPETUALS_PROGRAM_ID
76 | )[0];
77 |
78 | // default user to launch show basic pool data, etc
79 | export const DEFAULT_PERPS_USER = Keypair.fromSecretKey(
80 | Uint8Array.from([
81 | 130, 82, 70, 109, 220, 141, 128, 34, 238, 5, 80, 156, 116, 150, 24, 45, 33,
82 | 132, 119, 244, 40, 40, 201, 182, 195, 179, 90, 172, 51, 27, 110, 208, 61,
83 | 23, 43, 217, 131, 209, 127, 113, 93, 139, 35, 156, 34, 16, 94, 236, 175,
84 | 232, 174, 79, 209, 223, 86, 131, 148, 188, 126, 217, 19, 248, 236, 107,
85 | ])
86 | );
87 |
--------------------------------------------------------------------------------
/programs/perpetuals/tests/native/instructions/test_set_test_oracle_price.rs:
--------------------------------------------------------------------------------
1 | use {
2 | crate::utils::{self, pda},
3 | anchor_lang::{
4 | prelude::{AccountMeta, Pubkey},
5 | ToAccountMetas,
6 | },
7 | perpetuals::{
8 | instructions::SetTestOraclePriceParams,
9 | state::{multisig::Multisig, oracle::TestOracle},
10 | },
11 | solana_program_test::{BanksClientError, ProgramTestContext},
12 | solana_sdk::signer::{keypair::Keypair, Signer},
13 | };
14 |
15 | #[allow(clippy::too_many_arguments)]
16 | pub async fn test_set_test_oracle_price(
17 | program_test_ctx: &mut ProgramTestContext,
18 | admin: &Keypair,
19 | payer: &Keypair,
20 | pool_pda: &Pubkey,
21 | custody_pda: &Pubkey,
22 | oracle_pda: &Pubkey,
23 | params: SetTestOraclePriceParams,
24 | multisig_signers: &[&Keypair],
25 | ) -> std::result::Result<(), BanksClientError> {
26 | // ==== WHEN ==============================================================
27 | let multisig_pda = pda::get_multisig_pda().0;
28 | let perpetuals_pda = pda::get_perpetuals_pda().0;
29 |
30 | let multisig_account = utils::get_account::(program_test_ctx, multisig_pda).await;
31 |
32 | // One Tx per multisig signer
33 | for i in 0..multisig_account.min_signatures {
34 | let signer: &Keypair = multisig_signers[i as usize];
35 |
36 | let accounts_meta = {
37 | let accounts = perpetuals::accounts::SetTestOraclePrice {
38 | admin: admin.pubkey(),
39 | multisig: multisig_pda,
40 | perpetuals: perpetuals_pda,
41 | pool: *pool_pda,
42 | custody: *custody_pda,
43 | oracle_account: *oracle_pda,
44 | system_program: anchor_lang::system_program::ID,
45 | };
46 |
47 | let mut accounts_meta = accounts.to_account_metas(None);
48 |
49 | accounts_meta.push(AccountMeta {
50 | pubkey: signer.pubkey(),
51 | is_signer: true,
52 | is_writable: false,
53 | });
54 |
55 | accounts_meta
56 | };
57 |
58 | utils::create_and_execute_perpetuals_ix(
59 | program_test_ctx,
60 | accounts_meta,
61 | perpetuals::instruction::SetTestOraclePrice { params },
62 | Some(&payer.pubkey()),
63 | &[admin, payer, signer],
64 | )
65 | .await?;
66 | }
67 |
68 | // ==== THEN ==============================================================
69 | let test_oracle_account = utils::get_account::(program_test_ctx, *oracle_pda).await;
70 |
71 | assert_eq!(test_oracle_account.price, params.price);
72 | assert_eq!(test_oracle_account.expo, params.expo);
73 | assert_eq!(test_oracle_account.conf, params.conf);
74 | assert_eq!(test_oracle_account.publish_time, params.publish_time);
75 |
76 | Ok(())
77 | }
78 |
--------------------------------------------------------------------------------
/ui/src/lib/PositionAccount.tsx:
--------------------------------------------------------------------------------
1 | import { CustodyAccount } from "@/lib/CustodyAccount";
2 | import { TokenE } from "@/lib/Token";
3 | import { Position, Side } from "@/lib/types";
4 | import { BN } from "@project-serum/anchor";
5 | import { PublicKey } from "@solana/web3.js";
6 |
7 | export class PositionAccount {
8 | public owner: PublicKey;
9 | public pool: PublicKey;
10 | public custody: PublicKey;
11 | public lockCustody: PublicKey;
12 |
13 | public openTime: BN;
14 | public updateTime: BN;
15 |
16 | public side: Side;
17 | public price: BN;
18 | public sizeUsd: BN;
19 | public collateralUsd: BN;
20 | public unrealizedProfitUsd: BN;
21 | public unrealizedLossUsd: BN;
22 | public cumulativeInterestSnapshot: BN;
23 | public lockedAmount: BN;
24 | public collateralAmount: BN;
25 |
26 | public token: TokenE;
27 | public address: PublicKey;
28 | public oracleAccount: PublicKey;
29 |
30 | constructor(
31 | position: Position,
32 | address: PublicKey,
33 | custodies: Record
34 | ) {
35 | // console.log("printing entier new consturcture", position.openTime);
36 | this.owner = position.owner;
37 | this.pool = position.pool;
38 | this.custody = position.custody;
39 | this.lockCustody = position.lockCustody;
40 |
41 | this.openTime = position.openTime;
42 | this.updateTime = position.updateTime;
43 |
44 | this.side = position.side.hasOwnProperty("long") ? Side.Long : Side.Short;
45 | this.price = position.price;
46 | this.sizeUsd = position.sizeUsd;
47 | this.collateralUsd = position.collateralUsd;
48 | this.unrealizedProfitUsd = position.unrealizedProfitUsd;
49 | this.unrealizedLossUsd = position.unrealizedLossUsd;
50 | this.cumulativeInterestSnapshot = position.cumulativeInterestSnapshot;
51 | this.lockedAmount = position.lockedAmount;
52 | this.collateralAmount = position.collateralAmount;
53 |
54 | this.token = custodies[this.custody.toString()]?.getTokenE()!;
55 | this.address = address;
56 | this.oracleAccount =
57 | custodies[this.custody.toString()]?.oracle.oracleAccount!;
58 | }
59 |
60 | // TODO update leverage with pnl?
61 | getLeverage(): number {
62 | return this.sizeUsd.toNumber() / this.collateralUsd.toNumber();
63 | }
64 |
65 | // TODO fix getTimestamp to proper date
66 | getTimestamp(): string {
67 | const date = new Date(Number(this.openTime) * 1000);
68 | const dateString = date.toLocaleString();
69 |
70 | return dateString;
71 | }
72 |
73 | getCollateralUsd(): number {
74 | return Number(this.collateralUsd) / 10 ** 6;
75 | }
76 |
77 | getPrice(): number {
78 | return Number(this.price) / 10 ** 6;
79 | }
80 |
81 | getSizeUsd(): number {
82 | return Number(this.sizeUsd) / 10 ** 6;
83 | }
84 |
85 | getNetValue(pnl: number): number {
86 | // return this.getSizeUsd() - this.getCollateralUsd();
87 | return Number(this.getCollateralUsd()) + pnl;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/ui/src/components/TokenSelectorList.tsx:
--------------------------------------------------------------------------------
1 | import { twMerge } from "tailwind-merge";
2 | import CloseIcon from "@carbon/icons-react/lib/Close";
3 | import { cloneElement } from "react";
4 | import { TokenE, getTokenLabel, getTokenIcon, TOKEN_LIST } from "@/lib/Token";
5 | import { useGlobalStore } from "@/stores/store";
6 |
7 | function formatNumber(num: number) {
8 | const formatter = Intl.NumberFormat("en", {
9 | maximumFractionDigits: 2,
10 | minimumFractionDigits: 2,
11 | });
12 | return formatter.format(num);
13 | }
14 |
15 | interface Props {
16 | className?: string;
17 | onClose?(): void;
18 | onSelectToken?(token: TokenE): void;
19 | tokenList?: TokenE[];
20 | }
21 |
22 | export function TokenSelectorList(props: Props) {
23 | const stats = useGlobalStore((state) => state.priceStats);
24 |
25 | return (
26 |
30 |
e.stopPropagation()}
33 | >
34 |
35 | You Pay
36 |
37 |
38 |
39 |
40 |
41 | {(props.tokenList ? props.tokenList : TOKEN_LIST).map((token) => {
42 | const icon = getTokenIcon(token);
43 |
44 | return (
45 |
{
59 | props.onSelectToken?.(token);
60 | props.onClose?.();
61 | }}
62 | >
63 | {cloneElement(icon, {
64 | className: "h-10 w-10",
65 | })}
66 |
67 |
{token}
68 |
69 | {getTokenLabel(token)}
70 |
71 |
72 | {!!stats[token]?.currentPrice && (
73 |
74 | ${formatNumber(stats[token].currentPrice)}
75 |
76 | )}
77 |
78 | );
79 | })}
80 |
81 |
82 |
83 | );
84 | }
85 |
--------------------------------------------------------------------------------
/programs/perpetuals/src/instructions/set_test_oracle_price.rs:
--------------------------------------------------------------------------------
1 | //! SetTestOraclePrice instruction handler
2 |
3 | use {
4 | crate::state::{
5 | custody::Custody,
6 | multisig::{AdminInstruction, Multisig},
7 | oracle::TestOracle,
8 | perpetuals::Perpetuals,
9 | pool::Pool,
10 | },
11 | anchor_lang::prelude::*,
12 | };
13 |
14 | #[derive(Accounts)]
15 | pub struct SetTestOraclePrice<'info> {
16 | #[account(mut)]
17 | pub admin: Signer<'info>,
18 |
19 | #[account(
20 | mut,
21 | seeds = [b"multisig"],
22 | bump = multisig.load()?.bump
23 | )]
24 | pub multisig: AccountLoader<'info, Multisig>,
25 |
26 | #[account(
27 | seeds = [b"perpetuals"],
28 | bump = perpetuals.perpetuals_bump
29 | )]
30 | pub perpetuals: Box>,
31 |
32 | #[account(
33 | seeds = [b"pool",
34 | pool.name.as_bytes()],
35 | bump = pool.bump
36 | )]
37 | pub pool: Box>,
38 |
39 | #[account(
40 | seeds = [b"custody",
41 | pool.key().as_ref(),
42 | custody.mint.as_ref()],
43 | bump = custody.bump
44 | )]
45 | pub custody: Box>,
46 |
47 | #[account(
48 | init_if_needed,
49 | payer = admin,
50 | space = TestOracle::LEN,
51 | //constraint = oracle_account.key() == custody.oracle.oracle_account,
52 | seeds = [b"oracle_account",
53 | pool.key().as_ref(),
54 | custody.mint.as_ref()],
55 | bump
56 | )]
57 | pub oracle_account: Box>,
58 |
59 | system_program: Program<'info, System>,
60 | }
61 |
62 | #[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone)]
63 | pub struct SetTestOraclePriceParams {
64 | pub price: u64,
65 | pub expo: i32,
66 | pub conf: u64,
67 | pub publish_time: i64,
68 | }
69 |
70 | pub fn set_test_oracle_price<'info>(
71 | ctx: Context<'_, '_, '_, 'info, SetTestOraclePrice<'info>>,
72 | params: &SetTestOraclePriceParams,
73 | ) -> Result {
74 | // validate signatures
75 | let mut multisig = ctx.accounts.multisig.load_mut()?;
76 |
77 | let signatures_left = multisig.sign_multisig(
78 | &ctx.accounts.admin,
79 | &Multisig::get_account_infos(&ctx)[1..],
80 | &Multisig::get_instruction_data(AdminInstruction::SetTestOraclePrice, params)?,
81 | )?;
82 | if signatures_left > 0 {
83 | msg!(
84 | "Instruction has been signed but more signatures are required: {}",
85 | signatures_left
86 | );
87 | return Ok(signatures_left);
88 | }
89 |
90 | // update oracle data
91 | let oracle_account = ctx.accounts.oracle_account.as_mut();
92 | oracle_account.price = params.price;
93 | oracle_account.expo = params.expo;
94 | oracle_account.conf = params.conf;
95 | oracle_account.publish_time = params.publish_time;
96 |
97 | Ok(0)
98 | }
99 |
--------------------------------------------------------------------------------
/ui/src/components/PoolModal/PoolGeneralStats.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingSpinner } from "@/components/Icons/LoadingSpinner";
2 | import { PoolAccount } from "@/lib/PoolAccount";
3 | import { useGlobalStore } from "@/stores/store";
4 | import { formatNumberCommas } from "@/utils/formatters";
5 | import { getLiquidityBalance, getLiquidityShare } from "@/utils/retrieveData";
6 | import { twMerge } from "tailwind-merge";
7 |
8 | interface Props {
9 | pool: PoolAccount;
10 | className?: string;
11 | }
12 |
13 | export default function PoolGeneralStats(props: Props) {
14 | const stats = useGlobalStore((state) => state.priceStats);
15 |
16 | const userData = useGlobalStore((state) => state.userData);
17 |
18 | if (Object.keys(stats).length === 0 || props.pool.lpData === null) {
19 | return ;
20 | } else {
21 | return (
22 |
31 | {[
32 | {
33 | label: "Liquidity",
34 | value: `$${formatNumberCommas(props.pool.getLiquidities(stats))}`,
35 | },
36 | {
37 | label: "Volume",
38 | value: `$${formatNumberCommas(props.pool.getTradeVolumes())}`,
39 | },
40 | {
41 | label: "OI Long",
42 | value: (
43 | <>
44 | {`$${formatNumberCommas(props.pool.getOiLong())} `}
45 |
46 | >
47 | ),
48 | },
49 | {
50 | label: "OI Short",
51 | value: `$${formatNumberCommas(props.pool.getOiShort())}`,
52 | },
53 | {
54 | label: "Fees",
55 | value: `$${formatNumberCommas(props.pool.getFees())}`,
56 | },
57 | {
58 | label: "Your Liquidity",
59 | value: `$${formatNumberCommas(
60 | getLiquidityBalance(
61 | props.pool,
62 | userData.getUserLpBalance(props.pool.address.toString()),
63 | stats
64 | )
65 | )}`,
66 | },
67 | {
68 | label: "Your Share",
69 | value: `${formatNumberCommas(
70 | Number(
71 | getLiquidityShare(
72 | props.pool,
73 | userData.getUserLpBalance(props.pool.address.toString())
74 | )
75 | )
76 | )}%`,
77 | },
78 | ].map(({ label, value }, i) => (
79 |
83 |
{label}
84 |
{value}
85 |
86 | ))}
87 |
88 | );
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/programs/perpetuals/src/instructions/get_exit_price_and_fee.rs:
--------------------------------------------------------------------------------
1 | //! GetExitPriceAndFee instruction handler
2 |
3 | use {
4 | crate::state::{
5 | custody::Custody,
6 | oracle::OraclePrice,
7 | perpetuals::{Perpetuals, PriceAndFee},
8 | pool::Pool,
9 | position::Position,
10 | },
11 | anchor_lang::prelude::*,
12 | };
13 |
14 | #[derive(Accounts)]
15 | pub struct GetExitPriceAndFee<'info> {
16 | #[account(
17 | seeds = [b"perpetuals"],
18 | bump = perpetuals.perpetuals_bump
19 | )]
20 | pub perpetuals: Box>,
21 |
22 | #[account(
23 | seeds = [b"pool",
24 | pool.name.as_bytes()],
25 | bump = pool.bump
26 | )]
27 | pub pool: Box>,
28 |
29 | #[account(
30 | seeds = [b"position",
31 | position.owner.as_ref(),
32 | pool.key().as_ref(),
33 | custody.key().as_ref(),
34 | &[position.side as u8]],
35 | bump = position.bump
36 | )]
37 | pub position: Box>,
38 |
39 | #[account(
40 | seeds = [b"custody",
41 | pool.key().as_ref(),
42 | custody.mint.as_ref()],
43 | bump = custody.bump
44 | )]
45 | pub custody: Box>,
46 |
47 | /// CHECK: oracle account for the collateral token
48 | #[account(
49 | constraint = custody_oracle_account.key() == custody.oracle.oracle_account
50 | )]
51 | pub custody_oracle_account: AccountInfo<'info>,
52 | }
53 |
54 | #[derive(AnchorSerialize, AnchorDeserialize)]
55 | pub struct GetExitPriceAndFeeParams {}
56 |
57 | pub fn get_exit_price_and_fee(
58 | ctx: Context,
59 | _params: &GetExitPriceAndFeeParams,
60 | ) -> Result {
61 | // compute exit price and fee
62 | let position = &ctx.accounts.position;
63 | let pool = &ctx.accounts.pool;
64 | let curtime = ctx.accounts.perpetuals.get_time()?;
65 | let custody = ctx.accounts.custody.as_mut();
66 |
67 | let token_price = OraclePrice::new_from_oracle(
68 | custody.oracle.oracle_type,
69 | &ctx.accounts.custody_oracle_account.to_account_info(),
70 | custody.oracle.max_price_error,
71 | custody.oracle.max_price_age_sec,
72 | curtime,
73 | false,
74 | )?;
75 |
76 | let token_ema_price = OraclePrice::new_from_oracle(
77 | custody.oracle.oracle_type,
78 | &ctx.accounts.custody_oracle_account.to_account_info(),
79 | custody.oracle.max_price_error,
80 | custody.oracle.max_price_age_sec,
81 | curtime,
82 | custody.pricing.use_ema,
83 | )?;
84 |
85 | let price = pool.get_exit_price(&token_price, &token_ema_price, position.side, custody)?;
86 |
87 | let size = token_ema_price.get_token_amount(position.size_usd, custody.decimals)?;
88 |
89 | let fee = pool.get_exit_fee(size, custody)?;
90 |
91 | Ok(PriceAndFee { price, fee })
92 | }
93 |
--------------------------------------------------------------------------------
/ui/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import "@/styles/globals.css";
2 | import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
3 | import {
4 | ConnectionProvider,
5 | WalletProvider,
6 | } from "@solana/wallet-adapter-react";
7 | import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
8 | import {
9 | BackpackWalletAdapter,
10 | BraveWalletAdapter,
11 | CloverWalletAdapter,
12 | CoinbaseWalletAdapter,
13 | ExodusWalletAdapter,
14 | GlowWalletAdapter,
15 | HuobiWalletAdapter,
16 | LedgerWalletAdapter,
17 | PhantomWalletAdapter,
18 | SlopeWalletAdapter,
19 | SolletWalletAdapter,
20 | SolongWalletAdapter,
21 | TorusWalletAdapter,
22 | TrustWalletAdapter,
23 | } from "@solana/wallet-adapter-wallets";
24 | import { clusterApiUrl } from "@solana/web3.js";
25 | import { AppProps } from "next/app";
26 | import React, { FC, ReactNode, useMemo } from "react";
27 | import { ToastContainer } from "react-toastify";
28 | import "react-toastify/dist/ReactToastify.css";
29 |
30 | require("@solana/wallet-adapter-react-ui/styles.css");
31 |
32 | import { Navbar } from "@/components/Navbar";
33 | import { useHydrateStore } from "@/hooks/useHydrateStore";
34 |
35 | const StoreUpdater = () => {
36 | useHydrateStore();
37 | return null;
38 | };
39 |
40 | export default function MyApp({ Component, pageProps }: AppProps) {
41 | return (
42 |
43 |
54 |
55 |
56 |
57 |
58 | );
59 | }
60 |
61 | const Context: FC<{ children: ReactNode }> = ({ children }) => {
62 | // Can be set to 'devnet', 'testnet', or 'mainnet-beta'
63 | const network = WalletAdapterNetwork.Devnet;
64 | const endpoint = useMemo(() => clusterApiUrl(network), [network]);
65 | // const endpoint = useMemo(() => "http://localhost:8899");
66 |
67 | const wallets = useMemo(
68 | () => [
69 | new PhantomWalletAdapter(),
70 | new SlopeWalletAdapter(),
71 | new TorusWalletAdapter(),
72 | new LedgerWalletAdapter(),
73 | new BackpackWalletAdapter(),
74 | new BraveWalletAdapter(),
75 | new CloverWalletAdapter(),
76 | new CoinbaseWalletAdapter(),
77 | new ExodusWalletAdapter(),
78 | new GlowWalletAdapter(),
79 | new HuobiWalletAdapter(),
80 | new SolletWalletAdapter(),
81 | new SolongWalletAdapter(),
82 | new TrustWalletAdapter(),
83 | ],
84 | []
85 | );
86 |
87 | return (
88 |
89 |
90 |
91 | {children}
92 |
93 |
94 |
95 | );
96 | };
97 |
--------------------------------------------------------------------------------
/programs/perpetuals/src/instructions/withdraw_sol_fees.rs:
--------------------------------------------------------------------------------
1 | //! WithdrawSolFees instruction handler
2 |
3 | use {
4 | crate::{
5 | math,
6 | state::{
7 | multisig::{AdminInstruction, Multisig},
8 | perpetuals::Perpetuals,
9 | },
10 | },
11 | anchor_lang::prelude::*,
12 | solana_program::sysvar,
13 | };
14 |
15 | #[derive(Accounts)]
16 | pub struct WithdrawSolFees<'info> {
17 | #[account()]
18 | pub admin: Signer<'info>,
19 |
20 | #[account(
21 | mut,
22 | seeds = [b"multisig"],
23 | bump = multisig.load()?.bump
24 | )]
25 | pub multisig: AccountLoader<'info, Multisig>,
26 |
27 | /// CHECK: empty PDA, authority for token accounts
28 | #[account(
29 | seeds = [b"transfer_authority"],
30 | bump = perpetuals.transfer_authority_bump
31 | )]
32 | pub transfer_authority: AccountInfo<'info>,
33 |
34 | #[account(
35 | seeds = [b"perpetuals"],
36 | bump = perpetuals.perpetuals_bump
37 | )]
38 | pub perpetuals: Box>,
39 |
40 | /// CHECK: SOL fees receiving account
41 | #[account(
42 | mut,
43 | constraint = receiving_account.data_is_empty()
44 | )]
45 | pub receiving_account: AccountInfo<'info>,
46 | }
47 |
48 | #[derive(AnchorSerialize, AnchorDeserialize)]
49 | pub struct WithdrawSolFeesParams {
50 | pub amount: u64,
51 | }
52 |
53 | pub fn withdraw_sol_fees<'info>(
54 | ctx: Context<'_, '_, '_, 'info, WithdrawSolFees<'info>>,
55 | params: &WithdrawSolFeesParams,
56 | ) -> Result {
57 | // validate inputs
58 | if params.amount == 0 {
59 | return Err(ProgramError::InvalidArgument.into());
60 | }
61 |
62 | // validate signatures
63 | let mut multisig = ctx.accounts.multisig.load_mut()?;
64 |
65 | let signatures_left = multisig.sign_multisig(
66 | &ctx.accounts.admin,
67 | &Multisig::get_account_infos(&ctx)[1..],
68 | &Multisig::get_instruction_data(AdminInstruction::WithdrawSolFees, params)?,
69 | )?;
70 | if signatures_left > 0 {
71 | msg!(
72 | "Instruction has been signed but more signatures are required: {}",
73 | signatures_left
74 | );
75 | return Ok(signatures_left);
76 | }
77 |
78 | // transfer sol fees from the custody to the receiver
79 | let balance = ctx.accounts.transfer_authority.try_lamports()?;
80 | let min_balance = sysvar::rent::Rent::get().unwrap().minimum_balance(0);
81 | let available_balance = if balance > min_balance {
82 | math::checked_sub(balance, min_balance)?
83 | } else {
84 | 0
85 | };
86 |
87 | msg!(
88 | "Withdraw SOL fees: {} / {}",
89 | params.amount,
90 | available_balance
91 | );
92 |
93 | if available_balance < params.amount {
94 | return Err(ProgramError::InsufficientFunds.into());
95 | }
96 |
97 | Perpetuals::transfer_sol_from_owned(
98 | ctx.accounts.transfer_authority.to_account_info(),
99 | ctx.accounts.receiving_account.to_account_info(),
100 | params.amount,
101 | )?;
102 |
103 | Ok(0)
104 | }
105 |
--------------------------------------------------------------------------------
/ui/src/utils/transactionHelpers.ts:
--------------------------------------------------------------------------------
1 | import { checkIfAccountExists } from "@/utils/retrieveData";
2 | import {
3 | createAssociatedTokenAccountInstruction,
4 | createCloseAccountInstruction,
5 | createSyncNativeInstruction,
6 | getAssociatedTokenAddress,
7 | NATIVE_MINT,
8 | TOKEN_PROGRAM_ID,
9 | } from "@solana/spl-token";
10 | import {
11 | Connection,
12 | LAMPORTS_PER_SOL,
13 | PublicKey,
14 | SystemProgram,
15 | TransactionInstruction,
16 | } from "@solana/web3.js";
17 |
18 | export async function createAtaIfNeeded(
19 | publicKey: PublicKey,
20 | payer: PublicKey,
21 | mint: PublicKey,
22 | connection: Connection
23 | ): Promise {
24 | const associatedTokenAccount = await getAssociatedTokenAddress(
25 | mint,
26 | publicKey
27 | );
28 |
29 | console.log("creating ata", associatedTokenAccount.toString());
30 | if (!(await checkIfAccountExists(associatedTokenAccount, connection))) {
31 | console.log("ata doesn't exist");
32 | return createAssociatedTokenAccountInstruction(
33 | payer,
34 | associatedTokenAccount,
35 | publicKey,
36 | mint
37 | );
38 | }
39 |
40 | return null;
41 | }
42 |
43 | export async function wrapSolIfNeeded(
44 | publicKey: PublicKey,
45 | payer: PublicKey,
46 | connection: Connection,
47 | payAmount: number
48 | ): Promise {
49 | console.log("in wrap sol if needed");
50 | let preInstructions: TransactionInstruction[] = [];
51 |
52 | const associatedTokenAccount = await getAssociatedTokenAddress(
53 | NATIVE_MINT,
54 | publicKey
55 | );
56 |
57 | const balance =
58 | (await connection.getBalance(associatedTokenAccount)) / LAMPORTS_PER_SOL;
59 |
60 | if (balance < payAmount) {
61 | console.log("balance insufficient");
62 |
63 | preInstructions.push(
64 | SystemProgram.transfer({
65 | fromPubkey: publicKey,
66 | toPubkey: associatedTokenAccount,
67 | lamports: Math.floor((payAmount - balance) * LAMPORTS_PER_SOL * 3),
68 | })
69 | );
70 | preInstructions.push(
71 | createSyncNativeInstruction(associatedTokenAccount, TOKEN_PROGRAM_ID)
72 | );
73 | }
74 |
75 | return preInstructions.length > 0 ? preInstructions : null;
76 | }
77 |
78 | export async function unwrapSolIfNeeded(
79 | publicKey: PublicKey,
80 | payer: PublicKey,
81 | connection: Connection
82 | ): Promise {
83 | console.log("in unwrap sol if needed");
84 | let preInstructions: TransactionInstruction[] = [];
85 |
86 | const associatedTokenAccount = await getAssociatedTokenAddress(
87 | NATIVE_MINT,
88 | publicKey
89 | );
90 |
91 | // const balance =
92 | // (await connection.getBalance(associatedTokenAccount)) / LAMPORTS_PER_SOL;
93 | const balance = 1;
94 |
95 | if (balance > 0) {
96 | preInstructions.push(
97 | createCloseAccountInstruction(
98 | associatedTokenAccount,
99 | publicKey,
100 | publicKey
101 | )
102 | );
103 | }
104 |
105 | console.log("unwrap sol ix", preInstructions);
106 |
107 | return preInstructions.length > 0 ? preInstructions : null;
108 | }
109 |
--------------------------------------------------------------------------------
/programs/perpetuals/src/instructions/set_custody_config.rs:
--------------------------------------------------------------------------------
1 | //! SetCustodyConfig instruction handler
2 |
3 | use {
4 | crate::{
5 | error::PerpetualsError,
6 | state::{
7 | custody::{BorrowRateParams, Custody, Fees, OracleParams, PricingParams},
8 | multisig::{AdminInstruction, Multisig},
9 | perpetuals::Permissions,
10 | pool::{Pool, TokenRatios},
11 | },
12 | },
13 | anchor_lang::prelude::*,
14 | };
15 |
16 | #[derive(Accounts)]
17 | pub struct SetCustodyConfig<'info> {
18 | #[account()]
19 | pub admin: Signer<'info>,
20 |
21 | #[account(
22 | mut,
23 | seeds = [b"multisig"],
24 | bump = multisig.load()?.bump
25 | )]
26 | pub multisig: AccountLoader<'info, Multisig>,
27 |
28 | #[account(
29 | mut,
30 | seeds = [b"pool",
31 | pool.name.as_bytes()],
32 | bump = pool.bump
33 | )]
34 | pub pool: Box>,
35 |
36 | #[account(
37 | mut,
38 | seeds = [b"custody",
39 | pool.key().as_ref(),
40 | custody.mint.as_ref()],
41 | bump
42 | )]
43 | pub custody: Box>,
44 | }
45 |
46 | #[derive(AnchorSerialize, AnchorDeserialize, Clone)]
47 | pub struct SetCustodyConfigParams {
48 | pub is_stable: bool,
49 | pub oracle: OracleParams,
50 | pub pricing: PricingParams,
51 | pub permissions: Permissions,
52 | pub fees: Fees,
53 | pub borrow_rate: BorrowRateParams,
54 | pub ratios: Vec,
55 | }
56 |
57 | pub fn set_custody_config<'info>(
58 | ctx: Context<'_, '_, '_, 'info, SetCustodyConfig<'info>>,
59 | params: &SetCustodyConfigParams,
60 | ) -> Result {
61 | // validate inputs
62 | if params.ratios.len() != ctx.accounts.pool.ratios.len() {
63 | return Err(ProgramError::InvalidArgument.into());
64 | }
65 |
66 | // validate signatures
67 | let mut multisig = ctx.accounts.multisig.load_mut()?;
68 |
69 | let signatures_left = multisig.sign_multisig(
70 | &ctx.accounts.admin,
71 | &Multisig::get_account_infos(&ctx)[1..],
72 | &Multisig::get_instruction_data(AdminInstruction::SetCustodyConfig, params)?,
73 | )?;
74 | if signatures_left > 0 {
75 | msg!(
76 | "Instruction has been signed but more signatures are required: {}",
77 | signatures_left
78 | );
79 | return Ok(signatures_left);
80 | }
81 |
82 | // update pool data
83 | let pool = ctx.accounts.pool.as_mut();
84 | pool.ratios = params.ratios.clone();
85 | if !pool.validate() {
86 | return err!(PerpetualsError::InvalidPoolConfig);
87 | }
88 |
89 | // update custody data
90 | let custody = ctx.accounts.custody.as_mut();
91 | custody.is_stable = params.is_stable;
92 | custody.oracle = params.oracle;
93 | custody.pricing = params.pricing;
94 | custody.permissions = params.permissions;
95 | custody.fees = params.fees;
96 | custody.borrow_rate = params.borrow_rate;
97 |
98 | if !custody.validate() {
99 | err!(PerpetualsError::InvalidCustodyConfig)
100 | } else {
101 | Ok(0)
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/ui/src/actions/closePosition.ts:
--------------------------------------------------------------------------------
1 | import { CustodyAccount } from "@/lib/CustodyAccount";
2 | import { PoolAccount } from "@/lib/PoolAccount";
3 | import { PositionAccount } from "@/lib/PositionAccount";
4 | import { TokenE } from "@/lib/Token";
5 | import {
6 | getPerpetualProgramAndProvider,
7 | PERPETUALS_ADDRESS,
8 | TRANSFER_AUTHORITY,
9 | } from "@/utils/constants";
10 | import {
11 | automaticSendTransaction,
12 | manualSendTransaction,
13 | } from "@/utils/TransactionHandlers";
14 | import {
15 | createAtaIfNeeded,
16 | unwrapSolIfNeeded,
17 | } from "@/utils/transactionHelpers";
18 | import { BN } from "@project-serum/anchor";
19 | import { getAssociatedTokenAddress, TOKEN_PROGRAM_ID } from "@solana/spl-token";
20 | import { WalletContextState } from "@solana/wallet-adapter-react";
21 | import { Connection, TransactionInstruction } from "@solana/web3.js";
22 |
23 | export async function closePosition(
24 | walletContextState: WalletContextState,
25 | connection: Connection,
26 | pool: PoolAccount,
27 | position: PositionAccount,
28 | custody: CustodyAccount,
29 | price: BN
30 | ) {
31 | let { perpetual_program } = await getPerpetualProgramAndProvider(
32 | walletContextState
33 | );
34 | let publicKey = walletContextState.publicKey!;
35 |
36 | // TODO: need to take slippage as param , this is now for testing
37 | const adjustedPrice =
38 | position.side.toString() == "Long"
39 | ? price.mul(new BN(50)).div(new BN(100))
40 | : price.mul(new BN(150)).div(new BN(100));
41 |
42 | let userCustodyTokenAccount = await getAssociatedTokenAddress(
43 | custody.mint,
44 | publicKey
45 | );
46 |
47 | let preInstructions: TransactionInstruction[] = [];
48 |
49 | let ataIx = await createAtaIfNeeded(
50 | publicKey,
51 | publicKey,
52 | custody.mint,
53 | connection
54 | );
55 |
56 | if (ataIx) preInstructions.push(ataIx);
57 |
58 | let postInstructions: TransactionInstruction[] = [];
59 | let unwrapTx = await unwrapSolIfNeeded(publicKey, publicKey, connection);
60 | if (unwrapTx) postInstructions.push(...unwrapTx);
61 |
62 | let methodBuilder = await perpetual_program.methods
63 | .closePosition({
64 | price: adjustedPrice,
65 | })
66 | .accounts({
67 | owner: publicKey,
68 | receivingAccount: userCustodyTokenAccount,
69 | transferAuthority: TRANSFER_AUTHORITY,
70 | perpetuals: PERPETUALS_ADDRESS,
71 | pool: pool.address,
72 | position: position.address,
73 | custody: custody.address,
74 | custodyOracleAccount: custody.oracle.oracleAccount,
75 | custodyTokenAccount: custody.tokenAccount,
76 | tokenProgram: TOKEN_PROGRAM_ID,
77 | })
78 | .preInstructions(preInstructions);
79 |
80 | if (position.token == TokenE.SOL)
81 | methodBuilder = methodBuilder.postInstructions(postInstructions);
82 |
83 | try {
84 | // await automaticSendTransaction(
85 | // methodBuilder,
86 | // perpetual_program.provider.connection
87 | // );
88 | let tx = await methodBuilder.transaction();
89 | await manualSendTransaction(
90 | tx,
91 | publicKey,
92 | connection,
93 | walletContextState.signTransaction
94 | );
95 | } catch (err) {
96 | console.log(err);
97 | throw err;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/programs/perpetuals/tests/native/instructions/test_close_position.rs:
--------------------------------------------------------------------------------
1 | use {
2 | crate::utils::{self, pda},
3 | anchor_lang::{prelude::Pubkey, ToAccountMetas},
4 | bonfida_test_utils::ProgramTestContextExt,
5 | perpetuals::{instructions::ClosePositionParams, state::custody::Custody},
6 | solana_program_test::{BanksClientError, ProgramTestContext},
7 | solana_sdk::signer::{keypair::Keypair, Signer},
8 | };
9 |
10 | pub async fn test_close_position(
11 | program_test_ctx: &mut ProgramTestContext,
12 | owner: &Keypair,
13 | payer: &Keypair,
14 | pool_pda: &Pubkey,
15 | custody_token_mint: &Pubkey,
16 | position_pda: &Pubkey,
17 | params: ClosePositionParams,
18 | ) -> std::result::Result<(), BanksClientError> {
19 | // ==== WHEN ==============================================================
20 |
21 | // Prepare PDA and addresses
22 | let transfer_authority_pda = pda::get_transfer_authority_pda().0;
23 | let perpetuals_pda = pda::get_perpetuals_pda().0;
24 | let custody_pda = pda::get_custody_pda(pool_pda, custody_token_mint).0;
25 | let custody_token_account_pda =
26 | pda::get_custody_token_account_pda(pool_pda, custody_token_mint).0;
27 |
28 | let receiving_account_address =
29 | utils::find_associated_token_account(&owner.pubkey(), custody_token_mint).0;
30 |
31 | let custody_account = utils::get_account::(program_test_ctx, custody_pda).await;
32 | let custody_oracle_account_address = custody_account.oracle.oracle_account;
33 |
34 | // Save account state before tx execution
35 | let owner_receiving_account_before = program_test_ctx
36 | .get_token_account(receiving_account_address)
37 | .await
38 | .unwrap();
39 | let custody_token_account_before = program_test_ctx
40 | .get_token_account(custody_token_account_pda)
41 | .await
42 | .unwrap();
43 |
44 | utils::create_and_execute_perpetuals_ix(
45 | program_test_ctx,
46 | perpetuals::accounts::ClosePosition {
47 | owner: owner.pubkey(),
48 | receiving_account: receiving_account_address,
49 | transfer_authority: transfer_authority_pda,
50 | perpetuals: perpetuals_pda,
51 | pool: *pool_pda,
52 | position: *position_pda,
53 | custody: custody_pda,
54 | custody_oracle_account: custody_oracle_account_address,
55 | custody_token_account: custody_token_account_pda,
56 | token_program: anchor_spl::token::ID,
57 | }
58 | .to_account_metas(None),
59 | perpetuals::instruction::ClosePosition { params },
60 | Some(&payer.pubkey()),
61 | &[owner, payer],
62 | )
63 | .await?;
64 |
65 | // ==== THEN ==============================================================
66 | // Check the balance change
67 | {
68 | let owner_receiving_account_after = program_test_ctx
69 | .get_token_account(receiving_account_address)
70 | .await
71 | .unwrap();
72 | let custody_token_account_after = program_test_ctx
73 | .get_token_account(custody_token_account_pda)
74 | .await
75 | .unwrap();
76 |
77 | assert!(owner_receiving_account_after.amount > owner_receiving_account_before.amount);
78 | assert!(custody_token_account_after.amount < custody_token_account_before.amount);
79 | }
80 |
81 | Ok(())
82 | }
83 |
--------------------------------------------------------------------------------
/ui/src/components/LeverageSlider.tsx:
--------------------------------------------------------------------------------
1 | import CloseIcon from "@carbon/icons-react/lib/Close";
2 | import * as Slider from "@radix-ui/react-slider";
3 | import { twMerge } from "tailwind-merge";
4 |
5 | function clamp(num: number, min: number, max: number) {
6 | return Math.min(max, Math.max(num, min));
7 | }
8 |
9 | interface Props {
10 | className?: string;
11 | value: number;
12 | minLeverage: number;
13 | maxLeverage: number;
14 | onChange?(value: number): void;
15 | }
16 |
17 | export function LeverageSlider(props: Props) {
18 | return (
19 |
27 |
Leverage
28 |
1x
29 |
30 | props.onChange?.(values[0] || 1)}
36 | >
37 |
38 |
39 |
55 |
56 |
57 |
58 |
59 | {props.maxLeverage}x
60 |
61 |
73 | {
78 | const text = e.currentTarget.value;
79 | const number = parseFloat(text);
80 | props.onChange?.(
81 | Number.isNaN(number) ? 0 : clamp(number, 1, props.maxLeverage)
82 | );
83 | }}
84 | onBlur={(e) => {
85 | const text = e.currentTarget.value;
86 | const number = parseFloat(text);
87 | props.onChange?.(
88 | Number.isNaN(number) ? 1 : clamp(number, 1, props.maxLeverage)
89 | );
90 | }}
91 | />
92 | props.onChange?.(1)}>
93 |
102 |
103 |
104 |
105 | );
106 | }
107 |
--------------------------------------------------------------------------------
/ui/src/lib/CustodyAccount.tsx:
--------------------------------------------------------------------------------
1 | import { PublicKey } from "@solana/web3.js";
2 | import { tokenAddressToToken, TokenE } from "./Token";
3 | import {
4 | Assets,
5 | BorrowRateParams,
6 | BorrowRateState,
7 | Custody,
8 | Fees,
9 | OracleParams,
10 | PositionStats,
11 | PricingParams,
12 | Stats,
13 | TradeStats,
14 | Permissions,
15 | PriceStat,
16 | PriceStats,
17 | } from "./types";
18 |
19 | export class CustodyAccount {
20 | public pool: PublicKey;
21 | public mint: PublicKey;
22 | public tokenAccount: PublicKey;
23 | public decimals: number;
24 | public isStable: boolean;
25 | public oracle: OracleParams;
26 | public pricing: PricingParams;
27 | public permissions: Permissions;
28 | public fees: Fees;
29 | public borrowRate: BorrowRateParams;
30 |
31 | // dynamic variable;
32 | public assets: Assets;
33 | public collectedFees: Stats;
34 | public volumeStats: Stats;
35 | public tradeStats: TradeStats;
36 | public longPositions: PositionStats;
37 | public shortPositions: PositionStats;
38 | public borrowRateState: BorrowRateState;
39 |
40 | // bumps for address validatio;
41 | public bump: number;
42 | public tokenAccountBump: number;
43 |
44 | public address: PublicKey;
45 |
46 | constructor(custody: Custody, address: PublicKey) {
47 | this.pool = custody.pool;
48 | this.mint = custody.mint;
49 | this.tokenAccount = custody.tokenAccount;
50 | this.decimals = custody.decimals;
51 | this.isStable = custody.isStable;
52 | this.oracle = custody.oracle;
53 | this.pricing = custody.pricing;
54 | this.permissions = custody.permissions;
55 | this.fees = custody.fees;
56 | this.borrowRate = custody.borrowRate;
57 |
58 | console.log("custody assets", custody.assets);
59 | this.assets = custody.assets;
60 | this.collectedFees = custody.collectedFees;
61 | this.volumeStats = custody.volumeStats;
62 | this.tradeStats = custody.tradeStats;
63 | this.longPositions = custody.longPositions;
64 | this.shortPositions = custody.shortPositions;
65 | this.borrowRateState = custody.borrowRateState;
66 |
67 | this.bump = custody.bump;
68 | this.tokenAccountBump = custody.tokenAccountBump;
69 |
70 | this.address = address;
71 | }
72 |
73 | getTokenE(): TokenE {
74 | return tokenAddressToToken(this.mint.toString())!;
75 | }
76 |
77 | getCustodyLiquidity(stats: PriceStats): number {
78 | if (Object.values(stats).length === 0) {
79 | throw new Error("stats not loaded");
80 | }
81 | try {
82 | return (
83 | (stats[this.getTokenE()].currentPrice *
84 | Number(this.assets.owned.sub(this.assets.locked))) /
85 | 10 ** this.decimals
86 | );
87 | } catch (e) {
88 | console.log("stats error", e, stats);
89 | throw e;
90 | }
91 | }
92 |
93 | getCurrentWeight(stats: PriceStat, liquidity: number): number {
94 | let weight =
95 | (100 *
96 | stats.currentPrice *
97 | (Number(this.assets.owned) / 10 ** this.decimals)) /
98 | liquidity;
99 |
100 | return weight ? weight : 0;
101 | }
102 |
103 | getAmount(): number {
104 | return Number(this.assets.owned) / 10 ** this.decimals;
105 | }
106 |
107 | getAddFee(): number {
108 | return Number(this.fees.addLiquidity) / 100;
109 | }
110 | getUtilizationRate(): number {
111 | return Number(this.assets.owned) != 0
112 | ? 100 * Number(this.assets.locked.div(this.assets.owned))
113 | : 0;
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/programs/perpetuals/tests/native/instructions/test_add_pool.rs:
--------------------------------------------------------------------------------
1 | use {
2 | crate::utils::{self, pda},
3 | anchor_lang::{prelude::AccountMeta, ToAccountMetas},
4 | perpetuals::{
5 | instructions::AddPoolParams,
6 | state::{multisig::Multisig, perpetuals::Perpetuals, pool::Pool},
7 | },
8 | solana_program_test::{BanksClientError, ProgramTestContext},
9 | solana_sdk::signer::{keypair::Keypair, Signer},
10 | std::str::FromStr,
11 | };
12 |
13 | pub async fn test_add_pool(
14 | program_test_ctx: &mut ProgramTestContext,
15 | // Admin must be a part of the multisig
16 | admin: &Keypair,
17 | payer: &Keypair,
18 | pool_name: &str,
19 | multisig_signers: &[&Keypair],
20 | ) -> std::result::Result<
21 | (
22 | anchor_lang::prelude::Pubkey,
23 | u8,
24 | anchor_lang::prelude::Pubkey,
25 | u8,
26 | ),
27 | BanksClientError,
28 | > {
29 | // ==== WHEN ==============================================================
30 | let multisig_pda = pda::get_multisig_pda().0;
31 | let transfer_authority_pda = pda::get_transfer_authority_pda().0;
32 | let perpetuals_pda = pda::get_perpetuals_pda().0;
33 | let (pool_pda, pool_bump) = pda::get_pool_pda(String::from_str(pool_name).unwrap());
34 | let (lp_token_mint_pda, lp_token_mint_bump) = pda::get_lp_token_mint_pda(&pool_pda);
35 |
36 | let multisig_account = utils::get_account::(program_test_ctx, multisig_pda).await;
37 |
38 | // One Tx per multisig signer
39 | for i in 0..multisig_account.min_signatures {
40 | let signer: &Keypair = multisig_signers[i as usize];
41 |
42 | let accounts_meta = {
43 | let accounts = perpetuals::accounts::AddPool {
44 | admin: admin.pubkey(),
45 | multisig: multisig_pda,
46 | transfer_authority: transfer_authority_pda,
47 | perpetuals: perpetuals_pda,
48 | pool: pool_pda,
49 | lp_token_mint: lp_token_mint_pda,
50 | system_program: anchor_lang::system_program::ID,
51 | token_program: anchor_spl::token::ID,
52 | rent: solana_program::sysvar::rent::ID,
53 | };
54 |
55 | let mut accounts_meta = accounts.to_account_metas(None);
56 |
57 | accounts_meta.push(AccountMeta {
58 | pubkey: signer.pubkey(),
59 | is_signer: true,
60 | is_writable: false,
61 | });
62 |
63 | accounts_meta
64 | };
65 |
66 | utils::create_and_execute_perpetuals_ix(
67 | program_test_ctx,
68 | accounts_meta,
69 | perpetuals::instruction::AddPool {
70 | params: AddPoolParams {
71 | name: String::from_str(pool_name).unwrap(),
72 | },
73 | },
74 | Some(&payer.pubkey()),
75 | &[admin, payer, signer],
76 | )
77 | .await?;
78 | }
79 |
80 | // ==== THEN ==============================================================
81 | let pool_account = utils::get_account::(program_test_ctx, pool_pda).await;
82 |
83 | assert_eq!(pool_account.name.as_str(), pool_name);
84 | assert_eq!(pool_account.bump, pool_bump);
85 | assert_eq!(pool_account.lp_token_bump, lp_token_mint_bump);
86 |
87 | let perpetuals_account =
88 | utils::get_account::(program_test_ctx, perpetuals_pda).await;
89 |
90 | assert_eq!(*perpetuals_account.pools.last().unwrap(), pool_pda);
91 | assert_eq!(
92 | perpetuals_account.inception_time,
93 | pool_account.inception_time
94 | );
95 |
96 | Ok((pool_pda, pool_bump, lp_token_mint_pda, lp_token_mint_bump))
97 | }
98 |
--------------------------------------------------------------------------------
/programs/perpetuals/src/instructions/get_entry_price_and_fee.rs:
--------------------------------------------------------------------------------
1 | //! GetEntryPriceAndFee instruction handler
2 |
3 | use {
4 | crate::state::{
5 | custody::Custody,
6 | oracle::OraclePrice,
7 | perpetuals::{NewPositionPricesAndFee, Perpetuals},
8 | pool::Pool,
9 | position::{Position, Side},
10 | },
11 | anchor_lang::prelude::*,
12 | solana_program::program_error::ProgramError,
13 | };
14 |
15 | #[derive(Accounts)]
16 | pub struct GetEntryPriceAndFee<'info> {
17 | #[account(
18 | seeds = [b"perpetuals"],
19 | bump = perpetuals.perpetuals_bump
20 | )]
21 | pub perpetuals: Box>,
22 |
23 | #[account(
24 | seeds = [b"pool",
25 | pool.name.as_bytes()],
26 | bump = pool.bump
27 | )]
28 | pub pool: Box>,
29 |
30 | #[account(
31 | seeds = [b"custody",
32 | pool.key().as_ref(),
33 | custody.mint.as_ref()],
34 | bump = custody.bump
35 | )]
36 | pub custody: Box>,
37 |
38 | /// CHECK: oracle account for the collateral token
39 | #[account(
40 | constraint = custody_oracle_account.key() == custody.oracle.oracle_account
41 | )]
42 | pub custody_oracle_account: AccountInfo<'info>,
43 | }
44 |
45 | #[derive(AnchorSerialize, AnchorDeserialize)]
46 | pub struct GetEntryPriceAndFeeParams {
47 | collateral: u64,
48 | size: u64,
49 | side: Side,
50 | }
51 |
52 | pub fn get_entry_price_and_fee(
53 | ctx: Context,
54 | params: &GetEntryPriceAndFeeParams,
55 | ) -> Result {
56 | // validate inputs
57 | if params.collateral == 0 || params.size == 0 || params.side == Side::None {
58 | return Err(ProgramError::InvalidArgument.into());
59 | }
60 | let pool = &ctx.accounts.pool;
61 | let custody = ctx.accounts.custody.as_mut();
62 |
63 | // compute position price
64 | let curtime = ctx.accounts.perpetuals.get_time()?;
65 |
66 | let token_price = OraclePrice::new_from_oracle(
67 | custody.oracle.oracle_type,
68 | &ctx.accounts.custody_oracle_account.to_account_info(),
69 | custody.oracle.max_price_error,
70 | custody.oracle.max_price_age_sec,
71 | curtime,
72 | false,
73 | )?;
74 |
75 | let token_ema_price = OraclePrice::new_from_oracle(
76 | custody.oracle.oracle_type,
77 | &ctx.accounts.custody_oracle_account.to_account_info(),
78 | custody.oracle.max_price_error,
79 | custody.oracle.max_price_age_sec,
80 | curtime,
81 | custody.pricing.use_ema,
82 | )?;
83 |
84 | let min_price = if token_price < token_ema_price {
85 | token_price
86 | } else {
87 | token_ema_price
88 | };
89 |
90 | let entry_price = pool.get_entry_price(&token_price, &token_ema_price, params.side, custody)?;
91 |
92 | let size_usd = min_price.get_asset_amount_usd(params.size, custody.decimals)?;
93 | let collateral_usd = min_price.get_asset_amount_usd(params.collateral, custody.decimals)?;
94 |
95 | let position = Position {
96 | side: params.side,
97 | price: entry_price,
98 | size_usd,
99 | collateral_usd,
100 | cumulative_interest_snapshot: custody.get_cumulative_interest(curtime)?,
101 | ..Position::default()
102 | };
103 |
104 | let liquidation_price =
105 | pool.get_liquidation_price(&position, &token_ema_price, custody, curtime)?;
106 |
107 | let fee = pool.get_entry_fee(params.size, custody)?;
108 |
109 | Ok(NewPositionPricesAndFee {
110 | entry_price,
111 | liquidation_price,
112 | fee,
113 | })
114 | }
115 |
--------------------------------------------------------------------------------
/programs/perpetuals/src/instructions/test_init.rs:
--------------------------------------------------------------------------------
1 | //! TestInit instruction handler
2 |
3 | use {
4 | crate::{
5 | error::PerpetualsError,
6 | state::{multisig::Multisig, perpetuals::Perpetuals},
7 | },
8 | anchor_lang::prelude::*,
9 | anchor_spl::token::Token,
10 | solana_program::program_error::ProgramError,
11 | };
12 |
13 | #[derive(Accounts)]
14 | pub struct TestInit<'info> {
15 | #[account(mut)]
16 | pub upgrade_authority: Signer<'info>,
17 |
18 | #[account(
19 | init,
20 | payer = upgrade_authority,
21 | space = Multisig::LEN,
22 | seeds = [b"multisig"],
23 | bump
24 | )]
25 | pub multisig: AccountLoader<'info, Multisig>,
26 |
27 | /// CHECK: empty PDA, will be set as authority for token accounts
28 | #[account(
29 | init,
30 | payer = upgrade_authority,
31 | space = 0,
32 | seeds = [b"transfer_authority"],
33 | bump
34 | )]
35 | pub transfer_authority: AccountInfo<'info>,
36 |
37 | #[account(
38 | init,
39 | payer = upgrade_authority,
40 | space = Perpetuals::LEN,
41 | seeds = [b"perpetuals"],
42 | bump
43 | )]
44 | pub perpetuals: Box>,
45 |
46 | system_program: Program<'info, System>,
47 | token_program: Program<'info, Token>,
48 | // remaining accounts: 1 to Multisig::MAX_SIGNERS admin signers (read-only, unsigned)
49 | }
50 |
51 | #[derive(AnchorSerialize, AnchorDeserialize)]
52 | pub struct TestInitParams {
53 | pub min_signatures: u8,
54 | pub allow_swap: bool,
55 | pub allow_add_liquidity: bool,
56 | pub allow_remove_liquidity: bool,
57 | pub allow_open_position: bool,
58 | pub allow_close_position: bool,
59 | pub allow_pnl_withdrawal: bool,
60 | pub allow_collateral_withdrawal: bool,
61 | pub allow_size_change: bool,
62 | }
63 |
64 | pub fn test_init(ctx: Context, params: &TestInitParams) -> Result<()> {
65 | if !cfg!(feature = "test") {
66 | return err!(PerpetualsError::InvalidEnvironment);
67 | }
68 |
69 | // initialize multisig, this will fail if account is already initialized
70 | let mut multisig = ctx.accounts.multisig.load_init()?;
71 |
72 | multisig.set_signers(ctx.remaining_accounts, params.min_signatures)?;
73 |
74 | // record multisig PDA bump
75 | multisig.bump = *ctx
76 | .bumps
77 | .get("multisig")
78 | .ok_or(ProgramError::InvalidSeeds)?;
79 |
80 | // record perpetuals
81 | let perpetuals = ctx.accounts.perpetuals.as_mut();
82 | perpetuals.permissions.allow_swap = params.allow_swap;
83 | perpetuals.permissions.allow_add_liquidity = params.allow_add_liquidity;
84 | perpetuals.permissions.allow_remove_liquidity = params.allow_remove_liquidity;
85 | perpetuals.permissions.allow_open_position = params.allow_open_position;
86 | perpetuals.permissions.allow_close_position = params.allow_close_position;
87 | perpetuals.permissions.allow_pnl_withdrawal = params.allow_pnl_withdrawal;
88 | perpetuals.permissions.allow_collateral_withdrawal = params.allow_collateral_withdrawal;
89 | perpetuals.permissions.allow_size_change = params.allow_size_change;
90 | perpetuals.transfer_authority_bump = *ctx
91 | .bumps
92 | .get("transfer_authority")
93 | .ok_or(ProgramError::InvalidSeeds)?;
94 | perpetuals.perpetuals_bump = *ctx
95 | .bumps
96 | .get("perpetuals")
97 | .ok_or(ProgramError::InvalidSeeds)?;
98 | perpetuals.inception_time = if cfg!(feature = "test") {
99 | 0
100 | } else {
101 | perpetuals.get_time()?
102 | };
103 |
104 | if !perpetuals.validate() {
105 | return err!(PerpetualsError::InvalidPerpetualsConfig);
106 | }
107 |
108 | Ok(())
109 | }
110 |
--------------------------------------------------------------------------------
/ui/src/components/AirdropButton.tsx:
--------------------------------------------------------------------------------
1 | import { getAllUserData } from "@/hooks/storeHelpers/fetchUserData";
2 | import { CustodyAccount } from "@/lib/CustodyAccount";
3 | import { getTokenLabel, TokenE } from "@/lib/Token";
4 | import { useGlobalStore } from "@/stores/store";
5 | import { DEFAULT_PERPS_USER } from "@/utils/constants";
6 | import { checkIfAccountExists } from "@/utils/retrieveData";
7 | import {
8 | createAssociatedTokenAccountInstruction,
9 | createMintToCheckedInstruction,
10 | getAssociatedTokenAddress,
11 | } from "@solana/spl-token";
12 | import { useConnection, useWallet } from "@solana/wallet-adapter-react";
13 | import { Transaction } from "@solana/web3.js";
14 | import { SolidButton } from "./SolidButton";
15 |
16 | interface Props {
17 | className?: string;
18 | custody: CustodyAccount;
19 | }
20 | export default function AirdropButton(props: Props) {
21 | const { publicKey, signTransaction } = useWallet();
22 | const { connection } = useConnection();
23 |
24 | let mint = props.custody.mint;
25 |
26 | const poolData = useGlobalStore((state) => state.poolData);
27 | const setUserData = useGlobalStore((state) => state.setUserData);
28 |
29 | async function handleAirdrop() {
30 | if (!publicKey) return;
31 | if (mint.toString() === "So11111111111111111111111111111111111111112") {
32 | await connection.requestAirdrop(publicKey!, 1 * 10 ** 9);
33 | } else {
34 | let transaction = new Transaction();
35 |
36 | let associatedAccount = await getAssociatedTokenAddress(mint, publicKey);
37 |
38 | if (!(await checkIfAccountExists(associatedAccount, connection))) {
39 | transaction = transaction.add(
40 | createAssociatedTokenAccountInstruction(
41 | publicKey,
42 | associatedAccount,
43 | publicKey,
44 | mint
45 | )
46 | );
47 | }
48 |
49 | transaction = transaction.add(
50 | createMintToCheckedInstruction(
51 | mint, // mint
52 | associatedAccount, // ata
53 | DEFAULT_PERPS_USER.publicKey, // payer
54 | 100 * 10 ** 9, // amount
55 | 9 // decimals
56 | )
57 | );
58 |
59 | transaction.feePayer = publicKey;
60 | transaction.recentBlockhash = (
61 | await connection.getRecentBlockhash("finalized")
62 | ).blockhash;
63 |
64 | transaction.sign(DEFAULT_PERPS_USER);
65 |
66 | transaction = await signTransaction(transaction);
67 | const rawTransaction = transaction.serialize();
68 | let signature = await connection.sendRawTransaction(rawTransaction, {
69 | skipPreflight: false,
70 | });
71 | console.log(
72 | `sent raw, waiting : https://explorer.solana.com/tx/${signature}?cluster=devnet`
73 | );
74 | await connection.confirmTransaction(signature, "confirmed");
75 | console.log(
76 | `sent tx!!! :https://explorer.solana.com/tx/${signature}?cluster=devnet`
77 | );
78 | }
79 |
80 | const userData = await getAllUserData(connection, publicKey!, poolData);
81 | setUserData(userData);
82 | }
83 |
84 | if (props.custody.getTokenE() === TokenE.USDC) {
85 | return (
86 |
91 |
92 | Airdrop {'"'}
93 | {getTokenLabel(props.custody.getTokenE())}
94 | {'"'}
95 |
96 |
97 | );
98 | }
99 |
100 | return (
101 |
105 | Airdrop {'"'}
106 | {getTokenLabel(props.custody.getTokenE())}
107 | {'"'}
108 |
109 | );
110 | }
111 |
--------------------------------------------------------------------------------
/programs/perpetuals/src/instructions/get_remove_liquidity_amount_and_fee.rs:
--------------------------------------------------------------------------------
1 | //! GetRemoveLiquidityAmountAndFee instruction handler
2 |
3 | use {
4 | crate::{
5 | math,
6 | state::{
7 | custody::Custody,
8 | oracle::OraclePrice,
9 | perpetuals::{AmountAndFee, Perpetuals},
10 | pool::{AumCalcMode, Pool},
11 | },
12 | },
13 | anchor_lang::prelude::*,
14 | anchor_spl::token::Mint,
15 | solana_program::program_error::ProgramError,
16 | };
17 |
18 | #[derive(Accounts)]
19 | pub struct GetRemoveLiquidityAmountAndFee<'info> {
20 | #[account(
21 | seeds = [b"perpetuals"],
22 | bump = perpetuals.perpetuals_bump
23 | )]
24 | pub perpetuals: Box>,
25 |
26 | #[account(
27 | seeds = [b"pool",
28 | pool.name.as_bytes()],
29 | bump = pool.bump
30 | )]
31 | pub pool: Box>,
32 |
33 | #[account(
34 | seeds = [b"custody",
35 | pool.key().as_ref(),
36 | custody.mint.as_ref()],
37 | bump = custody.bump
38 | )]
39 | pub custody: Box>,
40 |
41 | /// CHECK: oracle account for the collateral token
42 | #[account(
43 | constraint = custody_oracle_account.key() == custody.oracle.oracle_account
44 | )]
45 | pub custody_oracle_account: AccountInfo<'info>,
46 |
47 | #[account(
48 | seeds = [b"lp_token_mint",
49 | pool.key().as_ref()],
50 | bump = pool.lp_token_bump
51 | )]
52 | pub lp_token_mint: Box>,
53 | }
54 |
55 | #[derive(AnchorSerialize, AnchorDeserialize)]
56 | pub struct GetRemoveLiquidityAmountAndFeeParams {
57 | lp_amount_in: u64,
58 | }
59 |
60 | pub fn get_remove_liquidity_amount_and_fee(
61 | ctx: Context,
62 | params: &GetRemoveLiquidityAmountAndFeeParams,
63 | ) -> Result {
64 | // validate inputs
65 | if params.lp_amount_in == 0 {
66 | return Err(ProgramError::InvalidArgument.into());
67 | }
68 | let pool = &ctx.accounts.pool;
69 | let custody = ctx.accounts.custody.as_mut();
70 | let token_id = pool.get_token_id(&custody.key())?;
71 |
72 | // compute position price
73 | let curtime = ctx.accounts.perpetuals.get_time()?;
74 |
75 | let token_price = OraclePrice::new_from_oracle(
76 | custody.oracle.oracle_type,
77 | &ctx.accounts.custody_oracle_account.to_account_info(),
78 | custody.oracle.max_price_error,
79 | custody.oracle.max_price_age_sec,
80 | curtime,
81 | false,
82 | )?;
83 |
84 | let token_ema_price = OraclePrice::new_from_oracle(
85 | custody.oracle.oracle_type,
86 | &ctx.accounts.custody_oracle_account.to_account_info(),
87 | custody.oracle.max_price_error,
88 | custody.oracle.max_price_age_sec,
89 | curtime,
90 | custody.pricing.use_ema,
91 | )?;
92 |
93 | let pool_amount_usd =
94 | pool.get_assets_under_management_usd(AumCalcMode::Min, ctx.remaining_accounts, curtime)?;
95 |
96 | let remove_amount_usd = math::checked_as_u64(math::checked_div(
97 | math::checked_mul(pool_amount_usd, params.lp_amount_in as u128)?,
98 | ctx.accounts.lp_token_mint.supply as u128,
99 | )?)?;
100 |
101 | let max_price = if token_price > token_ema_price {
102 | token_price
103 | } else {
104 | token_ema_price
105 | };
106 | let remove_amount = max_price.get_token_amount(remove_amount_usd, custody.decimals)?;
107 |
108 | let fee_amount =
109 | pool.get_remove_liquidity_fee(token_id, remove_amount, custody, &token_price)?;
110 |
111 | let transfer_amount = math::checked_sub(remove_amount, fee_amount)?;
112 |
113 | Ok(AmountAndFee {
114 | amount: transfer_amount,
115 | fee: fee_amount,
116 | })
117 | }
118 |
--------------------------------------------------------------------------------
/programs/perpetuals/tests/native/instructions/test_init.rs:
--------------------------------------------------------------------------------
1 | use {
2 | crate::utils::{self, pda},
3 | anchor_lang::{prelude::AccountMeta, ToAccountMetas},
4 | perpetuals::{
5 | instructions::InitParams,
6 | state::{multisig::Multisig, perpetuals::Perpetuals},
7 | },
8 | solana_program_test::{BanksClientError, ProgramTestContext},
9 | solana_sdk::signer::{keypair::Keypair, Signer},
10 | };
11 |
12 | pub async fn test_init(
13 | program_test_ctx: &mut ProgramTestContext,
14 | upgrade_authority: &Keypair,
15 | params: InitParams,
16 | multisig_signers: &[&Keypair],
17 | ) -> std::result::Result<(), BanksClientError> {
18 | // ==== WHEN ==============================================================
19 | let perpetuals_program_data_pda = pda::get_program_data_pda().0;
20 | let (multisig_pda, multisig_bump) = pda::get_multisig_pda();
21 | let (transfer_authority_pda, transfer_authority_bump) = pda::get_transfer_authority_pda();
22 | let (perpetuals_pda, perpetuals_bump) = pda::get_perpetuals_pda();
23 |
24 | let accounts_meta = {
25 | let accounts = perpetuals::accounts::Init {
26 | upgrade_authority: upgrade_authority.pubkey(),
27 | multisig: multisig_pda,
28 | transfer_authority: transfer_authority_pda,
29 | perpetuals: perpetuals_pda,
30 | perpetuals_program: perpetuals::ID,
31 | perpetuals_program_data: perpetuals_program_data_pda,
32 | system_program: anchor_lang::system_program::ID,
33 | token_program: anchor_spl::token::ID,
34 | };
35 |
36 | let mut accounts_meta = accounts.to_account_metas(None);
37 |
38 | for signer in multisig_signers {
39 | accounts_meta.push(AccountMeta {
40 | pubkey: signer.pubkey(),
41 | is_signer: true,
42 | is_writable: false,
43 | });
44 | }
45 |
46 | accounts_meta
47 | };
48 |
49 | utils::create_and_execute_perpetuals_ix(
50 | program_test_ctx,
51 | accounts_meta,
52 | perpetuals::instruction::Init { params },
53 | Some(&upgrade_authority.pubkey()),
54 | &[&[upgrade_authority], multisig_signers].concat(),
55 | )
56 | .await?;
57 |
58 | // ==== THEN ==============================================================
59 | let perpetuals_account =
60 | utils::get_account::(program_test_ctx, perpetuals_pda).await;
61 |
62 | // Assert permissions
63 | {
64 | let p = perpetuals_account.permissions;
65 |
66 | assert_eq!(p.allow_swap, params.allow_swap);
67 | assert_eq!(p.allow_add_liquidity, params.allow_add_liquidity);
68 | assert_eq!(p.allow_remove_liquidity, params.allow_remove_liquidity);
69 | assert_eq!(p.allow_open_position, params.allow_open_position);
70 | assert_eq!(p.allow_close_position, params.allow_close_position);
71 | assert_eq!(p.allow_pnl_withdrawal, params.allow_pnl_withdrawal);
72 | assert_eq!(
73 | p.allow_collateral_withdrawal,
74 | params.allow_collateral_withdrawal
75 | );
76 | assert_eq!(p.allow_size_change, params.allow_size_change);
77 | }
78 |
79 | assert_eq!(
80 | perpetuals_account.transfer_authority_bump,
81 | transfer_authority_bump
82 | );
83 | assert_eq!(perpetuals_account.perpetuals_bump, perpetuals_bump);
84 |
85 | let multisig_account = utils::get_account::(program_test_ctx, multisig_pda).await;
86 |
87 | // Assert multisig
88 | {
89 | assert_eq!(multisig_account.bump, multisig_bump);
90 | assert_eq!(multisig_account.min_signatures, params.min_signatures);
91 |
92 | // Check signers
93 | {
94 | for (i, signer) in multisig_signers.iter().enumerate() {
95 | assert_eq!(multisig_account.signers[i], signer.pubkey());
96 | }
97 | }
98 | }
99 |
100 | Ok(())
101 | }
102 |
--------------------------------------------------------------------------------
/ui/src/components/PoolSelector.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingSpinner } from "@/components/Icons/LoadingSpinner";
2 | import { PoolTokens } from "@/components/PoolTokens";
3 | import { PoolAccount } from "@/lib/PoolAccount";
4 | import { useGlobalStore } from "@/stores/store";
5 | import CheckmarkIcon from "@carbon/icons-react/lib/Checkmark";
6 | import ChevronDownIcon from "@carbon/icons-react/lib/ChevronDown";
7 | import * as Dropdown from "@radix-ui/react-dropdown-menu";
8 | import { useState } from "react";
9 | import { twMerge } from "tailwind-merge";
10 |
11 | interface Props {
12 | className?: string;
13 | pool: PoolAccount;
14 | onSelectPool?(pool: PoolAccount): void;
15 | }
16 |
17 | export function PoolSelector(props: Props) {
18 | const [open, setOpen] = useState(false);
19 |
20 | const poolData = useGlobalStore((state) => state.poolData);
21 |
22 | if (!props.pool) {
23 | return ;
24 | }
25 |
26 | return (
27 |
28 |
44 |
45 |
46 | {props.pool.name}
47 |
48 |
61 |
62 |
63 |
64 |
65 |
69 |
70 | {Object.values(poolData).map((pool) => (
71 | props.onSelectPool?.(pool)}
88 | >
89 |
90 |
91 |
92 | {pool.name}
93 |
94 |
95 | {pool.getTokenList().slice(0, 3).join(", ")}
96 | {pool.getTokenList().length > 3
97 | ? ` +${pool.getTokenList().length - 3} more`
98 | : ""}
99 |
100 |
101 | {pool.address === props.pool.address ? (
102 |
103 | ) : (
104 |
105 | )}
106 |
107 | ))}
108 |
109 |
110 |
111 | );
112 | }
113 |
--------------------------------------------------------------------------------
/programs/perpetuals/src/instructions/init.rs:
--------------------------------------------------------------------------------
1 | //! Init instruction handler
2 |
3 | use {
4 | crate::{
5 | error::PerpetualsError,
6 | state::{multisig::Multisig, perpetuals::Perpetuals},
7 | },
8 | anchor_lang::prelude::*,
9 | anchor_spl::token::Token,
10 | solana_program::program_error::ProgramError,
11 | };
12 |
13 | #[derive(Accounts)]
14 | pub struct Init<'info> {
15 | #[account(mut)]
16 | pub upgrade_authority: Signer<'info>,
17 |
18 | #[account(
19 | init,
20 | payer = upgrade_authority,
21 | space = Multisig::LEN,
22 | seeds = [b"multisig"],
23 | bump
24 | )]
25 | pub multisig: AccountLoader<'info, Multisig>,
26 |
27 | /// CHECK: empty PDA, will be set as authority for token accounts
28 | #[account(
29 | init,
30 | payer = upgrade_authority,
31 | space = 0,
32 | seeds = [b"transfer_authority"],
33 | bump
34 | )]
35 | pub transfer_authority: AccountInfo<'info>,
36 |
37 | #[account(
38 | init,
39 | payer = upgrade_authority,
40 | space = Perpetuals::LEN,
41 | seeds = [b"perpetuals"],
42 | bump
43 | )]
44 | pub perpetuals: Box>,
45 |
46 | #[account(
47 | constraint = perpetuals_program.programdata_address()? == Some(perpetuals_program_data.key())
48 | )]
49 | pub perpetuals_program: Program<'info, Perpetuals>,
50 |
51 | #[account(
52 | constraint = perpetuals_program_data.upgrade_authority_address == Some(upgrade_authority.key())
53 | )]
54 | pub perpetuals_program_data: Account<'info, ProgramData>,
55 |
56 | system_program: Program<'info, System>,
57 | token_program: Program<'info, Token>,
58 | // remaining accounts: 1 to Multisig::MAX_SIGNERS admin signers (read-only, unsigned)
59 | }
60 |
61 | #[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone)]
62 | pub struct InitParams {
63 | pub min_signatures: u8,
64 | pub allow_swap: bool,
65 | pub allow_add_liquidity: bool,
66 | pub allow_remove_liquidity: bool,
67 | pub allow_open_position: bool,
68 | pub allow_close_position: bool,
69 | pub allow_pnl_withdrawal: bool,
70 | pub allow_collateral_withdrawal: bool,
71 | pub allow_size_change: bool,
72 | }
73 |
74 | pub fn init(ctx: Context, params: &InitParams) -> Result<()> {
75 | // initialize multisig, this will fail if account is already initialized
76 | let mut multisig = ctx.accounts.multisig.load_init()?;
77 |
78 | multisig.set_signers(ctx.remaining_accounts, params.min_signatures)?;
79 |
80 | // record multisig PDA bump
81 | multisig.bump = *ctx
82 | .bumps
83 | .get("multisig")
84 | .ok_or(ProgramError::InvalidSeeds)?;
85 |
86 | // record perpetuals
87 | let perpetuals = ctx.accounts.perpetuals.as_mut();
88 | perpetuals.permissions.allow_swap = params.allow_swap;
89 | perpetuals.permissions.allow_add_liquidity = params.allow_add_liquidity;
90 | perpetuals.permissions.allow_remove_liquidity = params.allow_remove_liquidity;
91 | perpetuals.permissions.allow_open_position = params.allow_open_position;
92 | perpetuals.permissions.allow_close_position = params.allow_close_position;
93 | perpetuals.permissions.allow_pnl_withdrawal = params.allow_pnl_withdrawal;
94 | perpetuals.permissions.allow_collateral_withdrawal = params.allow_collateral_withdrawal;
95 | perpetuals.permissions.allow_size_change = params.allow_size_change;
96 | perpetuals.transfer_authority_bump = *ctx
97 | .bumps
98 | .get("transfer_authority")
99 | .ok_or(ProgramError::InvalidSeeds)?;
100 | perpetuals.perpetuals_bump = *ctx
101 | .bumps
102 | .get("perpetuals")
103 | .ok_or(ProgramError::InvalidSeeds)?;
104 | perpetuals.inception_time = perpetuals.get_time()?;
105 |
106 | if !perpetuals.validate() {
107 | return err!(PerpetualsError::InvalidPerpetualsConfig);
108 | }
109 |
110 | Ok(())
111 | }
112 |
--------------------------------------------------------------------------------
/programs/perpetuals/src/instructions/get_add_liquidity_amount_and_fee.rs:
--------------------------------------------------------------------------------
1 | //! GetAddLiquidityAmountAndFee instruction handler
2 |
3 | use {
4 | crate::{
5 | math,
6 | state::{
7 | custody::Custody,
8 | oracle::OraclePrice,
9 | perpetuals::{AmountAndFee, Perpetuals},
10 | pool::{AumCalcMode, Pool},
11 | },
12 | },
13 | anchor_lang::prelude::*,
14 | anchor_spl::token::Mint,
15 | solana_program::program_error::ProgramError,
16 | };
17 |
18 | #[derive(Accounts)]
19 | pub struct GetAddLiquidityAmountAndFee<'info> {
20 | #[account(
21 | seeds = [b"perpetuals"],
22 | bump = perpetuals.perpetuals_bump
23 | )]
24 | pub perpetuals: Box>,
25 |
26 | #[account(
27 | seeds = [b"pool",
28 | pool.name.as_bytes()],
29 | bump = pool.bump
30 | )]
31 | pub pool: Box>,
32 |
33 | #[account(
34 | seeds = [b"custody",
35 | pool.key().as_ref(),
36 | custody.mint.as_ref()],
37 | bump = custody.bump
38 | )]
39 | pub custody: Box>,
40 |
41 | /// CHECK: oracle account for the collateral token
42 | #[account(
43 | constraint = custody_oracle_account.key() == custody.oracle.oracle_account
44 | )]
45 | pub custody_oracle_account: AccountInfo<'info>,
46 |
47 | #[account(
48 | seeds = [b"lp_token_mint",
49 | pool.key().as_ref()],
50 | bump = pool.lp_token_bump
51 | )]
52 | pub lp_token_mint: Box>,
53 | }
54 |
55 | #[derive(AnchorSerialize, AnchorDeserialize)]
56 | pub struct GetAddLiquidityAmountAndFeeParams {
57 | amount_in: u64,
58 | }
59 |
60 | pub fn get_add_liquidity_amount_and_fee(
61 | ctx: Context,
62 | params: &GetAddLiquidityAmountAndFeeParams,
63 | ) -> Result {
64 | // validate inputs
65 | if params.amount_in == 0 {
66 | return Err(ProgramError::InvalidArgument.into());
67 | }
68 | let pool = &ctx.accounts.pool;
69 | let custody = ctx.accounts.custody.as_mut();
70 | let token_id = pool.get_token_id(&custody.key())?;
71 |
72 | // compute position price
73 | let curtime = ctx.accounts.perpetuals.get_time()?;
74 |
75 | let token_price = OraclePrice::new_from_oracle(
76 | custody.oracle.oracle_type,
77 | &ctx.accounts.custody_oracle_account.to_account_info(),
78 | custody.oracle.max_price_error,
79 | custody.oracle.max_price_age_sec,
80 | curtime,
81 | false,
82 | )?;
83 |
84 | let token_ema_price = OraclePrice::new_from_oracle(
85 | custody.oracle.oracle_type,
86 | &ctx.accounts.custody_oracle_account.to_account_info(),
87 | custody.oracle.max_price_error,
88 | custody.oracle.max_price_age_sec,
89 | curtime,
90 | custody.pricing.use_ema,
91 | )?;
92 |
93 | let fee_amount =
94 | pool.get_add_liquidity_fee(token_id, params.amount_in, custody, &token_price)?;
95 | let no_fee_amount = math::checked_sub(params.amount_in, fee_amount)?;
96 |
97 | let pool_amount_usd =
98 | pool.get_assets_under_management_usd(AumCalcMode::Max, ctx.remaining_accounts, curtime)?;
99 |
100 | let min_price = if token_price < token_ema_price {
101 | token_price
102 | } else {
103 | token_ema_price
104 | };
105 | let token_amount_usd = min_price.get_asset_amount_usd(no_fee_amount, custody.decimals)?;
106 |
107 | let lp_amount = if pool_amount_usd == 0 {
108 | token_amount_usd
109 | } else {
110 | math::checked_as_u64(math::checked_div(
111 | math::checked_mul(
112 | token_amount_usd as u128,
113 | ctx.accounts.lp_token_mint.supply as u128,
114 | )?,
115 | pool_amount_usd,
116 | )?)?
117 | };
118 |
119 | Ok(AmountAndFee {
120 | amount: lp_amount,
121 | fee: fee_amount,
122 | })
123 | }
124 |
--------------------------------------------------------------------------------
/programs/perpetuals/src/instructions/withdraw_fees.rs:
--------------------------------------------------------------------------------
1 | //! WithdrawFees instruction handler
2 |
3 | use {
4 | crate::{
5 | math,
6 | state::{
7 | custody::Custody,
8 | multisig::{AdminInstruction, Multisig},
9 | perpetuals::Perpetuals,
10 | pool::Pool,
11 | },
12 | },
13 | anchor_lang::prelude::*,
14 | anchor_spl::token::{Token, TokenAccount},
15 | };
16 |
17 | #[derive(Accounts)]
18 | pub struct WithdrawFees<'info> {
19 | #[account()]
20 | pub admin: Signer<'info>,
21 |
22 | #[account(
23 | mut,
24 | seeds = [b"multisig"],
25 | bump = multisig.load()?.bump
26 | )]
27 | pub multisig: AccountLoader<'info, Multisig>,
28 |
29 | /// CHECK: empty PDA, authority for token accounts
30 | #[account(
31 | seeds = [b"transfer_authority"],
32 | bump = perpetuals.transfer_authority_bump
33 | )]
34 | pub transfer_authority: AccountInfo<'info>,
35 |
36 | #[account(
37 | seeds = [b"perpetuals"],
38 | bump = perpetuals.perpetuals_bump
39 | )]
40 | pub perpetuals: Box>,
41 |
42 | #[account(
43 | mut,
44 | seeds = [b"pool",
45 | pool.name.as_bytes()],
46 | bump = pool.bump
47 | )]
48 | pub pool: Box>,
49 |
50 | #[account(
51 | mut,
52 | seeds = [b"custody",
53 | pool.key().as_ref(),
54 | custody.mint.key().as_ref()],
55 | bump = custody.bump
56 | )]
57 | pub custody: Box>,
58 |
59 | #[account(
60 | mut,
61 | seeds = [b"custody_token_account",
62 | pool.key().as_ref(),
63 | custody.mint.as_ref()],
64 | bump = custody.token_account_bump
65 | )]
66 | pub custody_token_account: Box>,
67 |
68 | #[account(
69 | mut,
70 | constraint = receiving_token_account.mint == custody_token_account.mint
71 | )]
72 | pub receiving_token_account: Box>,
73 |
74 | token_program: Program<'info, Token>,
75 | }
76 |
77 | #[derive(AnchorSerialize, AnchorDeserialize)]
78 | pub struct WithdrawFeesParams {
79 | pub amount: u64,
80 | }
81 |
82 | pub fn withdraw_fees<'info>(
83 | ctx: Context<'_, '_, '_, 'info, WithdrawFees<'info>>,
84 | params: &WithdrawFeesParams,
85 | ) -> Result {
86 | // validate inputs
87 | if params.amount == 0 {
88 | return Err(ProgramError::InvalidArgument.into());
89 | }
90 |
91 | // validate signatures
92 | let mut multisig = ctx.accounts.multisig.load_mut()?;
93 |
94 | let signatures_left = multisig.sign_multisig(
95 | &ctx.accounts.admin,
96 | &Multisig::get_account_infos(&ctx)[1..],
97 | &Multisig::get_instruction_data(AdminInstruction::WithdrawFees, params)?,
98 | )?;
99 | if signatures_left > 0 {
100 | msg!(
101 | "Instruction has been signed but more signatures are required: {}",
102 | signatures_left
103 | );
104 | return Ok(signatures_left);
105 | }
106 |
107 | // transfer token fees from the custody to the receiver
108 | let custody = ctx.accounts.custody.as_mut();
109 |
110 | msg!(
111 | "Withdraw token fees: {} / {}",
112 | params.amount,
113 | custody.assets.protocol_fees
114 | );
115 |
116 | if custody.assets.protocol_fees < params.amount {
117 | return Err(ProgramError::InsufficientFunds.into());
118 | }
119 | custody.assets.protocol_fees = math::checked_sub(custody.assets.protocol_fees, params.amount)?;
120 |
121 | ctx.accounts.perpetuals.transfer_tokens(
122 | ctx.accounts.custody_token_account.to_account_info(),
123 | ctx.accounts.receiving_token_account.to_account_info(),
124 | ctx.accounts.transfer_authority.to_account_info(),
125 | ctx.accounts.token_program.to_account_info(),
126 | params.amount,
127 | )?;
128 |
129 | Ok(0)
130 | }
131 |
--------------------------------------------------------------------------------
/programs/perpetuals/src/instructions/get_liquidation_price.rs:
--------------------------------------------------------------------------------
1 | //! GetLiquidationPrice instruction handler
2 |
3 | use {
4 | crate::{
5 | math,
6 | state::{
7 | custody::Custody, oracle::OraclePrice, perpetuals::Perpetuals, pool::Pool,
8 | position::Position,
9 | },
10 | },
11 | anchor_lang::prelude::*,
12 | };
13 |
14 | #[derive(Accounts)]
15 | pub struct GetLiquidationPrice<'info> {
16 | #[account(
17 | seeds = [b"perpetuals"],
18 | bump = perpetuals.perpetuals_bump
19 | )]
20 | pub perpetuals: Box>,
21 |
22 | #[account(
23 | seeds = [b"pool",
24 | pool.name.as_bytes()],
25 | bump = pool.bump
26 | )]
27 | pub pool: Box>,
28 |
29 | #[account(
30 | seeds = [b"position",
31 | position.owner.as_ref(),
32 | pool.key().as_ref(),
33 | custody.key().as_ref(),
34 | &[position.side as u8]],
35 | bump = position.bump
36 | )]
37 | pub position: Box>,
38 |
39 | #[account(
40 | seeds = [b"custody",
41 | pool.key().as_ref(),
42 | custody.mint.as_ref()],
43 | bump = custody.bump
44 | )]
45 | pub custody: Box>,
46 |
47 | /// CHECK: oracle account for the collateral token
48 | #[account(
49 | constraint = custody_oracle_account.key() == custody.oracle.oracle_account
50 | )]
51 | pub custody_oracle_account: AccountInfo<'info>,
52 | }
53 |
54 | #[derive(AnchorSerialize, AnchorDeserialize)]
55 | pub struct GetLiquidationPriceParams {
56 | add_collateral: u64,
57 | remove_collateral: u64,
58 | }
59 |
60 | pub fn get_liquidation_price(
61 | ctx: Context,
62 | params: &GetLiquidationPriceParams,
63 | ) -> Result {
64 | let custody = ctx.accounts.custody.as_mut();
65 | let curtime = ctx.accounts.perpetuals.get_time()?;
66 |
67 | let token_price = OraclePrice::new_from_oracle(
68 | custody.oracle.oracle_type,
69 | &ctx.accounts.custody_oracle_account.to_account_info(),
70 | custody.oracle.max_price_error,
71 | custody.oracle.max_price_age_sec,
72 | curtime,
73 | false,
74 | )?;
75 |
76 | let token_ema_price = OraclePrice::new_from_oracle(
77 | custody.oracle.oracle_type,
78 | &ctx.accounts.custody_oracle_account.to_account_info(),
79 | custody.oracle.max_price_error,
80 | custody.oracle.max_price_age_sec,
81 | curtime,
82 | custody.pricing.use_ema,
83 | )?;
84 |
85 | let min_price = if token_price < token_ema_price {
86 | token_price
87 | } else {
88 | token_ema_price
89 | };
90 |
91 | let mut position = ctx.accounts.position.clone();
92 | position.update_time = ctx.accounts.perpetuals.get_time()?;
93 |
94 | if params.add_collateral > 0 {
95 | let collateral_usd =
96 | min_price.get_asset_amount_usd(params.add_collateral, custody.decimals)?;
97 | position.collateral_usd = math::checked_add(position.collateral_usd, collateral_usd)?;
98 | position.collateral_amount =
99 | math::checked_add(position.collateral_amount, params.add_collateral)?;
100 | }
101 | if params.remove_collateral > 0 {
102 | let collateral_usd =
103 | min_price.get_asset_amount_usd(params.remove_collateral, custody.decimals)?;
104 | if collateral_usd >= position.collateral_usd
105 | || params.remove_collateral >= position.collateral_amount
106 | {
107 | return Err(ProgramError::InsufficientFunds.into());
108 | }
109 | position.collateral_usd = math::checked_sub(position.collateral_usd, collateral_usd)?;
110 | position.collateral_amount =
111 | math::checked_sub(position.collateral_amount, params.remove_collateral)?;
112 | }
113 |
114 | ctx.accounts
115 | .pool
116 | .get_liquidation_price(&position, &token_ema_price, custody, curtime)
117 | }
118 |
--------------------------------------------------------------------------------
/ui/src/lib/types.tsx:
--------------------------------------------------------------------------------
1 | import { TokenE } from "@/lib/Token";
2 | import { BN } from "@project-serum/anchor";
3 | import { PublicKey } from "@solana/web3.js";
4 |
5 | export interface Pool {
6 | name: string;
7 | custodies: PublicKey[];
8 | ratios: TokenRatios[];
9 | aumUsd: BN;
10 | bump: number;
11 | lpTokenBump: number;
12 | inceptionTime: BN;
13 | }
14 |
15 | export interface TokenRatios {
16 | target: BN;
17 | min: BN;
18 | max: BN;
19 | }
20 |
21 | export interface Custody {
22 | pool: PublicKey;
23 | mint: PublicKey;
24 | tokenAccount: PublicKey;
25 | decimals: number;
26 | isStable: boolean;
27 | oracle: OracleParams;
28 | pricing: PricingParams;
29 | permissions: Permissions;
30 | fees: Fees;
31 | borrowRate: BorrowRateParams;
32 |
33 | assets: Assets;
34 | collectedFees: Stats;
35 | volumeStats: Stats;
36 | tradeStats: TradeStats;
37 | longPositions: PositionStats;
38 | shortPositions: PositionStats;
39 | borrowRateState: BorrowRateState;
40 |
41 | bump: number;
42 | tokenAccountBump: number;
43 | }
44 |
45 | export interface BorrowRateParams {
46 | baseRate: BN;
47 | slope1: BN;
48 | slope2: BN;
49 | optimalUtilization: BN;
50 | }
51 |
52 | export interface BorrowRateState {
53 | currentRate: BN;
54 | cumulativeRate: BN;
55 | lastUpdate: BN;
56 | }
57 |
58 | export interface PositionStats {
59 | openPositions: BN;
60 | collateralUsd: BN;
61 | sizeUsd: BN;
62 | lockedAmount: BN;
63 | weightedLeverage: BN;
64 | totalLeverage: BN;
65 | cumulativeInterest: BN;
66 | cumulativeInterestSnapshot: BN;
67 | }
68 |
69 | export interface Assets {
70 | collateral: BN;
71 | protocolFees: BN;
72 | owned: BN;
73 | locked: BN;
74 | }
75 |
76 | export interface Stats {
77 | swapUsd: BN;
78 | addLiquidityUsd: BN;
79 | removeLiquidityUsd: BN;
80 | openPositionUsd: BN;
81 | closePositionUsd: BN;
82 | liquidationUsd: BN;
83 | }
84 |
85 | export interface Fees {
86 | mode: FeesMode;
87 | maxIncrease: BN;
88 | maxDecrease: BN;
89 | swap: BN;
90 | addLiquidity: BN;
91 | removeLiquidity: BN;
92 | openPosition: BN;
93 | closePosition: BN;
94 | liquidation: BN;
95 | protocolShare: BN;
96 | }
97 |
98 | export enum FeesMode {
99 | Fixed,
100 | Linear,
101 | }
102 |
103 | export interface OracleParams {
104 | oracleAccount: PublicKey;
105 | oracleType: OracleType;
106 | maxPriceError: BN;
107 | maxPriceAgeSec: number;
108 | }
109 |
110 | export enum OracleType {
111 | None,
112 | Test,
113 | Pyth,
114 | }
115 |
116 | export interface Permissions {
117 | allowSwap: boolean;
118 | allowAddLiquidity: boolean;
119 | allowRemoveLiquidity: boolean;
120 | allowOpenPosition: boolean;
121 | allowClosePosition: boolean;
122 | allowPnlWithdrawal: boolean;
123 | allowCollateralWithdrawal: boolean;
124 | allowSizeChange: boolean;
125 | }
126 |
127 | export interface PricingParams {
128 | useEma: boolean;
129 | tradeSpreadLong: BN;
130 | tradeSpreadShort: BN;
131 | swapSpread: BN;
132 | minInitialLeverage: BN;
133 | maxLeverage: BN;
134 | maxPayoffMult: BN;
135 | }
136 |
137 | export interface TradeStats {
138 | profitUsd: BN;
139 | lossUsd: BN;
140 | oiLongUsd: BN;
141 | oiShortUsd: BN;
142 | }
143 |
144 | export enum Side {
145 | None = "None",
146 | Long = "Long",
147 | Short = "Short",
148 | Swap = "Swap",
149 | }
150 |
151 | export enum Tab {
152 | Add,
153 | Remove,
154 | }
155 |
156 | export interface AccountMeta {
157 | pubkey: PublicKey;
158 | isSigner: boolean;
159 | isWritable: boolean;
160 | }
161 |
162 | export class TradeSide {
163 | static Long = { long: {} };
164 | static Short = { short: {} };
165 | }
166 |
167 | export interface Position {
168 | owner: PublicKey;
169 | pool: PublicKey;
170 | custody: PublicKey;
171 | lockCustody: PublicKey;
172 |
173 | openTime: BN;
174 | updateTime: BN;
175 |
176 | side: Side;
177 | price: BN;
178 | sizeUsd: BN;
179 | collateralUsd: BN;
180 | unrealizedProfitUsd: BN;
181 | unrealizedLossUsd: BN;
182 | cumulativeInterestSnapshot: BN;
183 | lockedAmount: BN;
184 | collateralAmount: BN;
185 | }
186 |
187 | export interface PriceStat {
188 | change24hr: number;
189 | currentPrice: number;
190 | high24hr: number;
191 | low24hr: number;
192 | }
193 |
194 | export type PriceStats = Record;
195 |
--------------------------------------------------------------------------------
/programs/perpetuals/tests/native/instructions/test_liquidate.rs:
--------------------------------------------------------------------------------
1 | use {
2 | crate::utils::{self, pda},
3 | anchor_lang::{prelude::Pubkey, ToAccountMetas},
4 | bonfida_test_utils::ProgramTestContextExt,
5 | perpetuals::{
6 | instructions::LiquidateParams,
7 | state::{custody::Custody, position::Position},
8 | },
9 | solana_program_test::{BanksClientError, ProgramTestContext},
10 | solana_sdk::signer::{keypair::Keypair, Signer},
11 | };
12 |
13 | pub async fn test_liquidate(
14 | program_test_ctx: &mut ProgramTestContext,
15 | liquidator: &Keypair,
16 | payer: &Keypair,
17 | pool_pda: &Pubkey,
18 | custody_token_mint: &Pubkey,
19 | position_pda: &Pubkey,
20 | ) -> std::result::Result<(), BanksClientError> {
21 | // ==== WHEN ==============================================================
22 | let owner = {
23 | let position_account =
24 | utils::get_account::(program_test_ctx, *position_pda).await;
25 | position_account.owner
26 | };
27 |
28 | // Prepare PDA and addresses
29 | let transfer_authority_pda = pda::get_transfer_authority_pda().0;
30 | let perpetuals_pda = pda::get_perpetuals_pda().0;
31 | let custody_pda = pda::get_custody_pda(pool_pda, custody_token_mint).0;
32 | let custody_token_account_pda =
33 | pda::get_custody_token_account_pda(pool_pda, custody_token_mint).0;
34 |
35 | let receiving_account_address =
36 | utils::find_associated_token_account(&owner, custody_token_mint).0;
37 |
38 | let rewards_receiving_account_address =
39 | utils::find_associated_token_account(&liquidator.pubkey(), custody_token_mint).0;
40 |
41 | let custody_account = utils::get_account::(program_test_ctx, custody_pda).await;
42 | let custody_oracle_account_address = custody_account.oracle.oracle_account;
43 |
44 | // Save account state before tx execution
45 | let receiving_account_before = program_test_ctx
46 | .get_token_account(receiving_account_address)
47 | .await
48 | .unwrap();
49 | let custody_token_account_before = program_test_ctx
50 | .get_token_account(custody_token_account_pda)
51 | .await
52 | .unwrap();
53 | let rewards_receiving_account_before = program_test_ctx
54 | .get_token_account(rewards_receiving_account_address)
55 | .await
56 | .unwrap();
57 |
58 | utils::create_and_execute_perpetuals_ix(
59 | program_test_ctx,
60 | perpetuals::accounts::Liquidate {
61 | signer: liquidator.pubkey(),
62 | rewards_receiving_account: rewards_receiving_account_address,
63 | receiving_account: receiving_account_address,
64 | transfer_authority: transfer_authority_pda,
65 | perpetuals: perpetuals_pda,
66 | pool: *pool_pda,
67 | position: *position_pda,
68 | custody: custody_pda,
69 | custody_oracle_account: custody_oracle_account_address,
70 | custody_token_account: custody_token_account_pda,
71 | token_program: anchor_spl::token::ID,
72 | }
73 | .to_account_metas(None),
74 | perpetuals::instruction::Liquidate {
75 | params: LiquidateParams {},
76 | },
77 | Some(&payer.pubkey()),
78 | &[liquidator, payer],
79 | )
80 | .await?;
81 |
82 | // ==== THEN ==============================================================
83 | // Check the balance change
84 | {
85 | let receiving_account_after = program_test_ctx
86 | .get_token_account(receiving_account_address)
87 | .await
88 | .unwrap();
89 | let custody_token_account_after = program_test_ctx
90 | .get_token_account(custody_token_account_pda)
91 | .await
92 | .unwrap();
93 | let rewards_receiving_account_after = program_test_ctx
94 | .get_token_account(rewards_receiving_account_address)
95 | .await
96 | .unwrap();
97 |
98 | assert!(receiving_account_after.amount >= receiving_account_before.amount);
99 | assert!(custody_token_account_after.amount <= custody_token_account_before.amount);
100 | assert!(rewards_receiving_account_after.amount > rewards_receiving_account_before.amount);
101 | }
102 |
103 | Ok(())
104 | }
105 |
--------------------------------------------------------------------------------