Refactor SDK and add formal documentation (#2)

- Add Typedoc integration to SDK
- Reorganize and finalize SDK structure
- Add documentation and categorize SDK public items (not all)
- Port quotes over from orca-sdk and provide sync options
- Port all utils over from orca-sdk and reorganize them into cleaner buckets
- Update test structure for integration tests and SDK tests
- Update README.md
- Add WhirlpoolClient to help construct the more complicated transactions
- Remove WhirlpoolIx Instruction's dependency on Transaction
This commit is contained in:
meep 2022-05-09 10:04:47 -07:00 committed by GitHub
parent 951f75d543
commit 1d7b482cbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
112 changed files with 10602 additions and 3604 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ node_modules
test-ledger/
sdk/dist/
sdk/node_modules
.vscode/

View File

@ -1,3 +0,0 @@
{
"files.insertFinalNewline": true
}

View File

@ -14,4 +14,4 @@ wallet = "~/.config/solana/id.json"
# program = "../metaplex-program-library/token-metadata/target/deploy/mpl_token_metadata.so"
[scripts]
test = "ts-mocha -p sdk/tests/tsconfig.json -t 1000000 sdk/tests/**/*.ts"
test = "ts-mocha -p sdk/tests/tsconfig.json -t 1000000 sdk/tests/**/*.test.ts"

View File

@ -6,6 +6,7 @@ This repository contains the Rust smart contract as well as the Typescript SDK (
The contract has been audited by Kudelski and Neodyme.
## Requirements
- Anchor 0.20.1
- Solana 1.9.3
- Rust 1.59.0
@ -18,12 +19,14 @@ Set up a valid Solana keypair at the path specified in the `wallet` in `Anchor.t
`$NODE_PATH` must be set to the `node_modules` directory of your global installs.
For example, using Node 16.10.0 installed through `nvm`, the $NODE_PATH is the following:
```
$ echo $NODE_PATH
/Users/<home_dir>/.nvm/versions/node/v16.10.0/lib/node_modules
```
## Usage
Instructions on how to interact with the Whirlpools contract is documented in the Orca Developer Portal.
## Tests
@ -33,7 +36,6 @@ Instructions on how to interact with the Whirlpools contract is documented in th
---
# Whirlpool SDK
Use the SDK to interact with a deployed Whirlpools program via Typescript.
@ -41,14 +43,25 @@ Use the SDK to interact with a deployed Whirlpools program via Typescript.
## Installation
In your package, run:
```
yarn add `@orca-so/whirlpool-sdk`
```
## Usage
Read instructions on how to use the SDK on the Orca Developer Portal.
## Run Typescript tests via local validator
In the whirlpools/sdk folder, run:
```
anchor test
```
## Generate TypeDoc
In the `sdk` folder, run `yarn run docs`
---

View File

@ -29,7 +29,7 @@ pub mod whirlpool {
/// Initializes a WhirlpoolsConfig account that hosts info & authorities
/// required to govern a set of Whirlpools.
///
/// # Parameters
/// ### Parameters
/// - `fee_authority` - Authority authorized to initialize fee-tiers and set customs fees.
/// - `collect_protocol_fees_authority` - Authority authorized to collect protocol fees.
/// - `reward_emissions_super_authority` - Authority authorized to set reward authorities in pools.
@ -52,12 +52,12 @@ pub mod whirlpool {
/// Initializes a Whirlpool account.
/// Fee rate is set to the default values on the config and supplied fee_tier.
///
/// # Parameters
/// ### Parameters
/// - `bumps` - The bump value when deriving the PDA of the Whirlpool address.
/// - `tick_spacing` - The desired tick spacing for this pool.
/// - `initial_sqrt_price` - The desired initial sqrt-price for this pool
///
/// # Special Errors
/// #### Special Errors
/// `InvalidTokenMintOrder` - The order of mints have to be ordered by
/// `SqrtPriceOutOfBounds` - provided initial_sqrt_price is not between 2^-64 to 2^64
///
@ -77,11 +77,11 @@ pub mod whirlpool {
/// Initializes a tick_array account to represent a tick-range in a Whirlpool.
///
/// # Parameters
/// ### Parameters
/// - `start_tick_index` - The starting tick index for this tick-array.
/// Has to be a multiple of TickArray size & the tick spacing of this pool.
///
/// # Special Errors
/// #### Special Errors
/// - `InvalidStartTick` - if the provided start tick is out of bounds or is not a multiple of
/// TICK_ARRAY_SIZE * tick spacing.
pub fn initialize_tick_array(
@ -93,15 +93,15 @@ pub mod whirlpool {
/// Initializes a fee_tier account usable by Whirlpools in a WhirlpoolConfig space.
///
/// # Authority
/// ### Authority
/// - "fee_authority" - Set authority in the WhirlpoolConfig
///
/// # Parameters
/// ### Parameters
/// - `tick_spacing` - The tick-spacing that this fee-tier suggests the default_fee_rate for.
/// - `default_fee_rate` - The default fee rate that a pool will use if the pool uses this
/// fee tier during initialization.
///
/// # Special Errors
/// #### Special Errors
/// - `FeeRateMaxExceeded` - If the provided default_fee_rate exceeds MAX_FEE_RATE.
pub fn initialize_fee_tier(
ctx: Context<InitializeFeeTier>,
@ -113,14 +113,14 @@ pub mod whirlpool {
/// Initialize reward for a Whirlpool. A pool can only support up to a set number of rewards.
///
/// # Authority
/// ### Authority
/// - "reward_authority" - assigned authority by the reward_super_authority for the specified
/// reward-index in this Whirlpool
///
/// # Parameters
/// ### Parameters
/// - `reward_index` - The reward index that we'd like to initialize. (0 <= index <= NUM_REWARDS)
///
/// # Special Errors
/// #### Special Errors
/// - `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized
/// index in this pool, or exceeds NUM_REWARDS, or
/// all reward slots for this pool has been initialized.
@ -130,15 +130,15 @@ pub mod whirlpool {
/// Set the reward emissions for a reward in a Whirlpool.
///
/// # Authority
/// ### Authority
/// - "reward_authority" - assigned authority by the reward_super_authority for the specified
/// reward-index in this Whirlpool
///
/// # Parameters
/// ### Parameters
/// - `reward_index` - The reward index (0 <= index <= NUM_REWARDS) that we'd like to modify.
/// - `emissions_per_second_x64` - The amount of rewards emitted in this pool.
///
/// # Special Errors
/// #### Special Errors
/// - `RewardVaultAmountInsufficient` - The amount of rewards in the reward vault cannot emit
/// more than a day of desired emissions.
/// - `InvalidTimestamp` - Provided timestamp is not in order with the previous timestamp.
@ -160,11 +160,11 @@ pub mod whirlpool {
/// Open a position in a Whirlpool. A unique token will be minted to represent the position
/// in the users wallet. The position will start off with 0 liquidity.
///
/// # Parameters
/// ### Parameters
/// - `tick_lower_index` - The tick specifying the lower end of the position range.
/// - `tick_upper_index` - The tick specifying the upper end of the position range.
///
/// # Special Errors
/// #### Special Errors
/// - `InvalidTickIndex` - If a provided tick is out of bounds, out of order or not a multiple of
/// the tick-spacing in this pool.
pub fn open_position(
@ -185,11 +185,11 @@ pub mod whirlpool {
/// in the users wallet. Additional Metaplex metadata is appended to identify the token.
/// The position will start off with 0 liquidity.
///
/// # Parameters
/// ### Parameters
/// - `tick_lower_index` - The tick specifying the lower end of the position range.
/// - `tick_upper_index` - The tick specifying the upper end of the position range.
///
/// # Special Errors
/// #### Special Errors
/// - `InvalidTickIndex` - If a provided tick is out of bounds, out of order or not a multiple of
/// the tick-spacing in this pool.
pub fn open_position_with_metadata(
@ -206,17 +206,17 @@ pub mod whirlpool {
);
}
/// Add liquidity to a position in the Whirlpool.
/// Add liquidity to a position in the Whirlpool. This call also updates the position's accrued fees and rewards.
///
/// # Authority
/// ### Authority
/// - `position_authority` - authority that owns the token corresponding to this desired position.
///
/// # Parameters
/// ### Parameters
/// - `liquidity_amount` - The total amount of Liquidity the user is willing to deposit.
/// - `token_max_a` - The maximum amount of tokenA the user is willing to deposit.
/// - `token_max_b` - The maximum amount of tokenB the user is willing to deposit.
///
/// # Special Errors
/// #### Special Errors
/// - `LiquidityZero` - Provided liquidity amount is zero.
/// - `LiquidityTooHigh` - Provided liquidity exceeds u128::max.
/// - `TokenMaxExceeded` - The required token to perform this operation exceeds the user defined amount.
@ -234,17 +234,17 @@ pub mod whirlpool {
);
}
/// Withdraw liquidity from a position in the Whirlpool.
/// Withdraw liquidity from a position in the Whirlpool. This call also updates the position's accrued fees and rewards.
///
/// # Authority
/// ### Authority
/// - `position_authority` - authority that owns the token corresponding to this desired position.
///
/// # Parameters
/// ### Parameters
/// - `liquidity_amount` - The total amount of Liquidity the user desires to withdraw.
/// - `token_min_a` - The minimum amount of tokenA the user is willing to withdraw.
/// - `token_min_b` - The minimum amount of tokenB the user is willing to withdraw.
///
/// # Special Errors
/// #### Special Errors
/// - `LiquidityZero` - Provided liquidity amount is zero.
/// - `LiquidityTooHigh` - Provided liquidity exceeds u128::max.
/// - `TokenMinSubceeded` - The required token to perform this operation subceeds the user defined amount.
@ -264,7 +264,7 @@ pub mod whirlpool {
/// Update the accrued fees and rewards for a position.
///
/// # Special Errors
/// #### Special Errors
/// - `TickNotFound` - Provided tick array account does not contain the tick for this position.
/// - `LiquidityZero` - Position has zero liquidity and therefore already has the most updated fees and reward values.
pub fn update_fees_and_rewards(ctx: Context<UpdateFeesAndRewards>) -> ProgramResult {
@ -273,7 +273,7 @@ pub mod whirlpool {
/// Collect fees accrued for this position.
///
/// # Authority
/// ### Authority
/// - `position_authority` - authority that owns the token corresponding to this desired position.
pub fn collect_fees(ctx: Context<CollectFees>) -> ProgramResult {
return instructions::collect_fees::handler(ctx);
@ -281,7 +281,7 @@ pub mod whirlpool {
/// Collect rewards accrued for this position.
///
/// # Authority
/// ### Authority
/// - `position_authority` - authority that owns the token corresponding to this desired position.
pub fn collect_reward(ctx: Context<CollectReward>, reward_index: u8) -> ProgramResult {
return instructions::collect_reward::handler(ctx, reward_index);
@ -289,7 +289,7 @@ pub mod whirlpool {
/// Collect the protocol fees accrued in this Whirlpool
///
/// # Authority
/// ### Authority
/// - `collect_protocol_fees_authority` - assigned authority in the WhirlpoolConfig that can collect protocol fees
pub fn collect_protocol_fees(ctx: Context<CollectProtocolFees>) -> ProgramResult {
return instructions::collect_protocol_fees::handler(ctx);
@ -297,12 +297,25 @@ pub mod whirlpool {
/// Perform a swap in this Whirlpool
///
/// # Parameters
/// - `amount`
/// - `other_amount_threshold`
/// - `sqrt_price_limit`
/// - `exact_input`
/// - `a_to_b`
/// ### Authority
/// - "token_authority" - The authority to withdraw tokens from the input token account.
///
/// ### Parameters
/// - `amount` - The amount of input or output token to swap from (depending on exact_input).
/// - `other_amount_threshold` - The maximum/minimum of input/output token to swap into (depending on exact_input).
/// - `sqrt_price_limit` - The maximum/minimum price the swap will swap to.
/// - `exact_input` - Specifies the token the parameter `amount`represents. If true, the amount represents the input token of the swap.
/// - `a_to_b` - The direction of the swap. True if swapping from A to B. False if swapping from B to A.
///
/// #### Special Errors
/// - `ZeroTradableAmount` - User provided parameter `amount` is 0.
/// - `InvalidSqrtPriceLimitDirection` - User provided parameter `sqrt_price_limit` does not match the direction of the trade.
/// - `SqrtPriceOutOfBounds` - User provided parameter `sqrt_price_limit` is over Whirlppool's max/min bounds for sqrt-price.
/// - `InvalidTickArraySequence` - User provided tick-arrays are not in sequential order required to proceed in this trade direction.
/// - `TickArraySequenceInvalidIndex` - The swap loop attempted to access an invalid array index during the query of the next initialized tick.
/// - `TickArrayIndexOutofBounds` - The swap loop attempted to access an invalid array index during tick crossing.
/// - `LiquidityOverflow` - Liquidity value overflowed 128bits during tick crossing.
/// - `InvalidTickSpacing` - The swap pool was initialized with tick-spacing of 0.
pub fn swap(
ctx: Context<Swap>,
amount: u64,
@ -321,6 +334,13 @@ pub mod whirlpool {
);
}
/// Close a position in a Whirlpool. Burns the position token in the owner's wallet.
///
/// ### Authority
/// - "position_authority" - The authority that owns the position token.
///
/// #### Special Errors
/// - `ClosePositionNotEmpty` - The provided position account is not empty.
pub fn close_position(ctx: Context<ClosePosition>) -> ProgramResult {
return instructions::close_position::handler(ctx);
}
@ -328,14 +348,14 @@ pub mod whirlpool {
/// Set the default_fee_rate for a FeeTier
/// Only the current fee authority has permission to invoke this instruction.
///
/// # Authority
/// ### Authority
/// - "fee_authority" - Set authority in the WhirlpoolConfig
///
/// # Parameters
/// ### Parameters
/// - `default_fee_rate` - The default fee rate that a pool will use if the pool uses this
/// fee tier during initialization.
///
/// # Special Errors
/// #### Special Errors
/// - `FeeRateMaxExceeded` - If the provided default_fee_rate exceeds MAX_FEE_RATE.
pub fn set_default_fee_rate(
ctx: Context<SetDefaultFeeRate>,
@ -348,13 +368,13 @@ pub mod whirlpool {
/// Protocol fee rate is represented as a basis point.
/// Only the current fee authority has permission to invoke this instruction.
///
/// # Authority
/// ### Authority
/// - "fee_authority" - Set authority that can modify pool fees in the WhirlpoolConfig
///
/// # Parameters
/// ### Parameters
/// - `default_protocol_fee_rate` - Rate that is referenced during the initialization of a Whirlpool using this config.
///
/// # Special Errors
/// #### Special Errors
/// - `ProtocolFeeRateMaxExceeded` - If the provided default_protocol_fee_rate exceeds MAX_PROTOCOL_FEE_RATE.
pub fn set_default_protocol_fee_rate(
ctx: Context<SetDefaultProtocolFeeRate>,
@ -370,14 +390,14 @@ pub mod whirlpool {
/// Fee rate is represented as hundredths of a basis point.
/// Only the current fee authority has permission to invoke this instruction.
///
/// # Authority
/// ### Authority
/// - "fee_authority" - Set authority that can modify pool fees in the WhirlpoolConfig
///
/// # Parameters
/// ### Parameters
/// - `fee_rate` - The rate that the pool will use to calculate fees going onwards.
///
/// # Special Errors
/// - `FeeRateMaxExceeded` - If the provided default_fee_rate exceeds MAX_FEE_RATE.
/// #### Special Errors
/// - `FeeRateMaxExceeded` - If the provided fee_rate exceeds MAX_FEE_RATE.
pub fn set_fee_rate(ctx: Context<SetFeeRate>, fee_rate: u16) -> ProgramResult {
return instructions::set_fee_rate::handler(ctx, fee_rate);
}
@ -386,13 +406,13 @@ pub mod whirlpool {
/// Protocol fee rate is represented as a basis point.
/// Only the current fee authority has permission to invoke this instruction.
///
/// # Authority
/// ### Authority
/// - "fee_authority" - Set authority that can modify pool fees in the WhirlpoolConfig
///
/// # Parameters
/// ### Parameters
/// - `protocol_fee_rate` - The rate that the pool will use to calculate protocol fees going onwards.
///
/// # Special Errors
/// #### Special Errors
/// - `ProtocolFeeRateMaxExceeded` - If the provided default_protocol_fee_rate exceeds MAX_PROTOCOL_FEE_RATE.
pub fn set_protocol_fee_rate(
ctx: Context<SetProtocolFeeRate>,
@ -404,9 +424,9 @@ pub mod whirlpool {
/// Sets the fee authority for a WhirlpoolConfig.
/// The fee authority can set the fee & protocol fee rate for individual pools or
/// set the default fee rate for newly minted pools.
/// Only the current collect fee authority has permission to invoke this instruction.
/// Only the current fee authority has permission to invoke this instruction.
///
/// # Authority
/// ### Authority
/// - "fee_authority" - Set authority that can modify pool fees in the WhirlpoolConfig
pub fn set_fee_authority(ctx: Context<SetFeeAuthority>) -> ProgramResult {
return instructions::set_fee_authority::handler(ctx);
@ -415,7 +435,7 @@ pub mod whirlpool {
/// Sets the fee authority to collect protocol fees for a WhirlpoolConfig.
/// Only the current collect protocol fee authority has permission to invoke this instruction.
///
/// # Authority
/// ### Authority
/// - "fee_authority" - Set authority that can collect protocol fees in the WhirlpoolConfig
pub fn set_collect_protocol_fees_authority(
ctx: Context<SetCollectProtocolFeesAuthority>,
@ -426,8 +446,13 @@ pub mod whirlpool {
/// Set the whirlpool reward authority at the provided `reward_index`.
/// Only the current reward authority for this reward index has permission to invoke this instruction.
///
/// # Authority
/// ### Authority
/// - "reward_authority" - Set authority that can control reward emission for this particular reward.
///
/// #### Special Errors
/// - `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized
/// index in this pool, or exceeds NUM_REWARDS, or
/// all reward slots for this pool has been initialized.
pub fn set_reward_authority(
ctx: Context<SetRewardAuthority>,
reward_index: u8,
@ -438,8 +463,13 @@ pub mod whirlpool {
/// Set the whirlpool reward authority at the provided `reward_index`.
/// Only the current reward super authority has permission to invoke this instruction.
///
/// # Authority
/// ### Authority
/// - "reward_authority" - Set authority that can control reward emission for this particular reward.
///
/// #### Special Errors
/// - `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized
/// index in this pool, or exceeds NUM_REWARDS, or
/// all reward slots for this pool has been initialized.
pub fn set_reward_authority_by_super_authority(
ctx: Context<SetRewardAuthorityBySuperAuthority>,
reward_index: u8,
@ -451,7 +481,7 @@ pub mod whirlpool {
/// Only the current reward super authority has permission to invoke this instruction.
/// This instruction will not change the authority on any `WhirlpoolRewardInfo` whirlpool rewards.
///
/// # Authority
/// ### Authority
/// - "reward_emissions_super_authority" - Set authority that can control reward authorities for all pools in this config space.
pub fn set_reward_emissions_super_authority(
ctx: Context<SetRewardEmissionsSuperAuthority>,

View File

@ -99,9 +99,9 @@ impl<'info> SwapTickSequence<'info> {
///
/// # Returns
/// - `(usize, i32, &mut Tick)`: The array_index which the next initialized index was found, the next initialized tick-index & a mutable reference to that tick
///
/// - `InvalidTickArraySequence`: - Unable to find the next initialized index with the provided tick-array sequence.
/// - Provided tick-arrays are not in sequential order to the trade direction.
/// - `TickArraySequenceInvalidIndex` - The swap loop provided an invalid array index to query the next tick in.
/// - `InvalidTickArraySequence`: - User provided tick-arrays are not in sequential order required to proceed in this trade direction.
pub fn get_next_initialized_tick_index(
&self,
tick_index: i32,

18
sdk/jest.config.js Normal file
View File

@ -0,0 +1,18 @@
module.exports = {
"roots": [
"<rootDir>/src",
"<rootDir>/tests/sdk"
],
"testMatch": [
"**/__tests__/**/*.+(ts|tsx|js)",
"**/?(*.)+(spec|test).+(ts|tsx|js)"
],
"transform": {
"^.+\\.(ts|tsx)$": "ts-jest"
},
globals: {
"ts-jest": {
tsconfig: "./tests/tsconfig.json"
}
}
}

View File

@ -1,24 +1,29 @@
{
"name": "@orca-so/whirlpool-client-sdk",
"version": "0.0.8",
"name": "@orca-so/whirlpools-sdk",
"version": "1.0.0",
"description": "Provides functions to generate instructions needed for Orca's Whirlpool contracts",
"license": "MIT",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"dependencies": {
"@metaplex-foundation/mpl-token-metadata": "1.2.5",
"@orca-so/common-sdk": "^0.0.4",
"@project-serum/anchor": "^0.20.1",
"@solana/spl-token": "^0.1.8",
"decimal.js": "^10.3.1"
"decimal.js": "^10.3.1",
"tiny-invariant": "^1.2.0"
},
"devDependencies": {
"@types/decimal.js": "^7.4.0",
"@types/mocha": "^9.0.0",
"@types/jest": "^26.0.24",
"@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.26.0",
"chai": "^4.3.4",
"eslint-config-prettier": "^8.3.0",
"mocha": "^9.0.3",
"jest": "^27.0.6",
"ts-jest": "^27.0.3",
"prettier": "^2.3.2",
"typescript": "^4.5.5"
},
@ -26,7 +31,9 @@
"build": "tsc -p src",
"watch": "tsc -w -p src",
"prepublishOnly": "yarn build",
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write"
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write",
"test": "jest",
"docs": "npx typedoc --excludePrivate --categorizeByGroup false --tsconfig src/tsconfig.json"
},
"lint-staged": {
"*.{ts,md}": "yarn run prettier-format"

View File

@ -1,379 +0,0 @@
import { WhirlpoolContext } from "./context";
import { PublicKey } from "@solana/web3.js";
import { WhirlpoolConfigAccount } from "./types/public/account-types";
import { TransactionBuilder } from "./utils/transactions/transactions-builder";
import { buildInitializeConfigIx } from "./instructions/initialize-config-ix";
import {
ClosePositionParams,
CollectFeesParams,
CollectProtocolFeesParams,
CollectRewardParams,
InitConfigParams,
InitializeRewardParams,
InitPoolParams,
InitTickArrayParams,
OpenPositionParams,
SetCollectProtocolFeesAuthorityParams,
SetFeeAuthorityParams,
SetRewardAuthorityBySuperAuthorityParams,
parsePosition,
parseTickArray,
parseWhirlpool,
parseWhirlpoolsConfig,
SetRewardAuthorityParams,
SetRewardEmissionsParams,
SetRewardEmissionsSuperAuthorityParams,
SwapParams,
UpdateFeesAndRewardsParams,
SetFeeRateParams,
SetDefaultProtocolFeeRateParams,
SetProtocolFeeRateParams,
SetDefaultFeeRateParams,
DecreaseLiquidityParams,
IncreaseLiquidityParams,
InitFeeTierParams,
} from ".";
import { buildInitPoolIx } from "./instructions/initialize-pool-ix";
import {
FeeTierData,
PositionData,
TickArrayData,
WhirlpoolData,
} from "./types/public/anchor-types";
import {
buildOpenPositionIx,
buildOpenPositionWithMetadataIx,
} from "./instructions/open-position-ix";
import { buildInitTickArrayIx } from "./instructions/initialize-tick-array-ix";
import { buildIncreaseLiquidityIx } from "./instructions/increase-liquidity-ix";
import { buildCollectFeesIx } from "./instructions/collect-fees-ix";
import { buildCollectRewardIx } from "./instructions/collect-reward-ix";
import { buildSwapIx } from "./instructions/swap-ix";
import { buildInitializeRewardIx } from "./instructions/initialize-reward-ix";
import { buildSetRewardEmissionsSuperAuthorityIx } from "./instructions/set-reward-emissions-super-authority-ix";
import { buildSetRewardAuthorityIx } from "./instructions/set-reward-authority-ix";
import { buildSetRewardEmissionsIx } from "./instructions/set-reward-emissions-ix";
import { buildClosePositionIx } from "./instructions/close-position-ix";
import { buildSetRewardAuthorityBySuperAuthorityIx } from "./instructions/set-reward-authority-by-super-authority-ix";
import { buildSetFeeAuthorityIx } from "./instructions/set-fee-authority-ix";
import { buildSetCollectProtocolFeesAuthorityIx } from "./instructions/set-collect-protocol-fees-authority-ix";
import { buildUpdateFeesAndRewardsIx } from "./instructions/update-fees-and-rewards-ix";
import { buildCollectProtocolFeesIx } from "./instructions/collect-protocol-fees-ix";
import { buildDecreaseLiquidityIx } from "./instructions/decrease-liquidity-ix";
import { buildSetFeeRateIx } from "./instructions/set-fee-rate-ix";
import { buildSetDefaultProtocolFeeRateIx } from "./instructions/set-default-protocol-fee-rate-ix";
import { buildSetDefaultFeeRateIx } from "./instructions/set-default-fee-rate-ix";
import { buildSetProtocolFeeRateIx } from "./instructions/set-protocol-fee-rate-ix";
import { buildInitializeFeeTier } from "./instructions/initialize-fee-tier";
import { Decimal } from "decimal.js";
// Global rules for Decimals
// - 40 digits of precision for the largest number
// - 20 digits of precision for the smallest number
// - Always round towards 0 to mirror smart contract rules
Decimal.set({ precision: 40, toExpPos: 40, toExpNeg: -20, rounding: 1 });
/**
* WhirlpoolClient provides a portal to perform admin-type tasks on the Whirlpool protocol.
*/
export class WhirlpoolClient {
readonly context: WhirlpoolContext;
public constructor(context: WhirlpoolContext) {
this.context = context;
}
/**
* Construct a TransactionBuilder to initialize a WhirlpoolConfig account with the provided parameters.
* @param params Parameters to configure the initialized WhirlpoolConfig account
* @returns A TransactionBuilder to initialize a WhirlpoolConfig account with the provided parameters.
*/
public initConfigTx(params: InitConfigParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildInitializeConfigIx(this.context, params)
);
}
/**
* Fetches and parses a WhirlpoolConfig account.
* @param poolPubKey A public key of a WhirlpoolConfig account
* @returns A WhirlpoolConfig type containing the parameters stored on the account
*/
public async getConfig(configPubKey: PublicKey): Promise<WhirlpoolConfigAccount> {
const program = this.context.program;
const account = await program.account.whirlpoolsConfig.fetch(configPubKey);
// TODO: If we feel nice we can build a builder or something instead of casting
return account as WhirlpoolConfigAccount;
}
/**
* Parses a WhirlpoolConfig account.
* @param data A buffer containing data fetched from an account
* @returns A WhirlpoolConfig type containing the parameters stored on the account
*/
public parseConfig(data: Buffer): WhirlpoolConfigAccount | null {
return parseWhirlpoolsConfig(data);
}
/**
* Construct a TransactionBuilder to initialize a FeeTier account with the provided parameters.
* @param params Parameters to configure the initialized FeeTier account
* @returns A TransactionBuilder to initialize a FeeTier account with the provided parameters.
*/
public initFeeTierTx(params: InitFeeTierParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildInitializeFeeTier(this.context, params)
);
}
/**
* Fetches and parses a FeeTier account.
* @param feeTierKey A public key of a FeeTier account
* @returns A FeeTier type containing the parameters stored on the account
*/
public async getFeeTier(feeTierKey: PublicKey): Promise<FeeTierData> {
const program = this.context.program;
const feeTierAccount = await program.account.feeTier.fetch(feeTierKey);
return feeTierAccount as unknown as FeeTierData;
}
/**
* Construct a TransactionBuilder to initialize a Whirlpool account with the provided parameters.
* @param params Parameters to configure the initialized Whirlpool account
* @returns A TransactionBuilder to initialize a Whirlpool account with the provided parameters.
*/
public initPoolTx(params: InitPoolParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildInitPoolIx(this.context, params)
);
}
/**
* Fetches and parses a Whirlpool account.
* @param poolPubKey A public key of a Whirlpool account
* @returns A Whirlpool type containing the parameters stored on the account
*/
public async getPool(poolKey: PublicKey): Promise<WhirlpoolData> {
const program = this.context.program;
const whirlpoolAccount = await program.account.whirlpool.fetch(poolKey);
return whirlpoolAccount as unknown as WhirlpoolData;
}
/**
* Parses a Whirlpool account.
* @param data A buffer containing data fetched from an account
* @returns A Whirlpool type containing the parameters stored on the account
*/
public parsePool(data: Buffer): WhirlpoolData | null {
return parseWhirlpool(data);
}
/**
* Construct a TransactionBuilder to open a Position account.
* @param params Parameters to configure the initialized Position account.
* @returns A TransactionBuilder to initialize a Position account with the provided parameters.
*/
public openPositionTx(params: OpenPositionParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildOpenPositionIx(this.context, params)
);
}
/**
* Construct a TransactionBuilder to open a Position account with metadata.
* @param params Parameters to configure the initialized Position account.
* @returns A TransactionBuilder to initialize a Position account with the provided parameters.
*/
public openPositionWithMetadataTx(params: Required<OpenPositionParams>): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildOpenPositionWithMetadataIx(this.context, params)
);
}
public closePositionTx(params: ClosePositionParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildClosePositionIx(this.context, params)
);
}
/**
* Fetches a Position account.
* @param positionKey The public key of the Position account
* @returns A Position type containing the parameters stored on the account
*/
public async getPosition(positionKey: PublicKey): Promise<PositionData> {
const positionAccount = await this.context.program.account.position.fetch(positionKey);
return positionAccount as unknown as PositionData;
}
/**
* Parses a Position account.
* @param data A buffer containing data fetched from an account
* @returns A Position type containing the parameters stored on the account
*/
public parsePosition(data: Buffer): PositionData | null {
return parsePosition(data);
}
/*
* Construct a TransactionBuilder to initialize a TickArray account with the provided parameters.
* @param params Parameters to configure the initialized TickArray account
* @returns A TransactionBuilder to initialize a TickArray account with the provided parameters.
*/
public initTickArrayTx(params: InitTickArrayParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildInitTickArrayIx(this.context, params)
);
}
/**
* Fetches and parses a TickArray account. Account is used to store Ticks for a Whirlpool.
* @param arrayPubKey A public key of a TickArray account
* @returns A TickArrayData type containing the parameters stored on the account
*/
public async getTickArray(arrayPubKey: PublicKey): Promise<TickArrayData> {
const program = this.context.program;
const tickArrayAccount = await program.account.tickArray.fetch(arrayPubKey);
return tickArrayAccount as unknown as TickArrayData;
}
/**
* Parses a TickArray account.
* @param data A buffer containing data fetched from an account
* @returns A Position type containing the parameters stored on the account
*/
public parseTickArray(data: Buffer): TickArrayData | null {
return parseTickArray(data);
}
public initializeRewardTx(params: InitializeRewardParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildInitializeRewardIx(this.context, params)
);
}
public setRewardEmissionsTx(params: SetRewardEmissionsParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildSetRewardEmissionsIx(this.context, params)
);
}
/**
* Construct a TransactionBuilder to increase the liquidity of a Position.
* @param params Parameters to configure the increase liquidity instruction
* @returns A TransactionBuilder containing one increase liquidity instruction
*/
public increaseLiquidityTx(params: IncreaseLiquidityParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildIncreaseLiquidityIx(this.context, params)
);
}
public decreaseLiquidityTx(params: DecreaseLiquidityParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildDecreaseLiquidityIx(this.context, params)
);
}
public updateFeesAndRewards(params: UpdateFeesAndRewardsParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildUpdateFeesAndRewardsIx(this.context, params)
);
}
/**
* Construct a TransactionBuilder to collect the fees for a Position.
* @param params Parameters to configure the collect fees instruction
* @returns A TransactionBuilder containing one collect fees instruction
*/
public collectFeesTx(params: CollectFeesParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildCollectFeesIx(this.context, params)
);
}
/**
* Construct a TransactionBuilder to collect a reward at the specified index for a Position.
* @param params Parameters to configure the collect reward instruction
* @returns A TransactionBuilder containing one collect reward instruction
*/
public collectRewardTx(params: CollectRewardParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildCollectRewardIx(this.context, params)
);
}
public collectProtocolFeesTx(params: CollectProtocolFeesParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildCollectProtocolFeesIx(this.context, params)
);
}
public swapTx(params: SwapParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildSwapIx(this.context, params)
);
}
public setRewardEmissionsSuperAuthorityTx(
params: SetRewardEmissionsSuperAuthorityParams
): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildSetRewardEmissionsSuperAuthorityIx(this.context, params)
);
}
public setRewardAuthorityTx(params: SetRewardAuthorityParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildSetRewardAuthorityIx(this.context, params)
);
}
public setRewardAuthorityBySuperAuthorityTx(
params: SetRewardAuthorityBySuperAuthorityParams
): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildSetRewardAuthorityBySuperAuthorityIx(this.context, params)
);
}
public setFeeAuthorityTx(params: SetFeeAuthorityParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildSetFeeAuthorityIx(this.context, params)
);
}
public setCollectProtocolFeesAuthorityTx(
params: SetCollectProtocolFeesAuthorityParams
): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildSetCollectProtocolFeesAuthorityIx(this.context, params)
);
}
public setFeeRateIx(params: SetFeeRateParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildSetFeeRateIx(this.context, params)
);
}
public setProtocolFeeRateIx(params: SetProtocolFeeRateParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildSetProtocolFeeRateIx(this.context, params)
);
}
public setDefaultFeeRateIx(params: SetDefaultFeeRateParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildSetDefaultFeeRateIx(this.context, params)
);
}
public setDefaultProtocolFeeRateIx(params: SetDefaultProtocolFeeRateParams): TransactionBuilder {
return new TransactionBuilder(this.context.provider).addInstruction(
buildSetDefaultProtocolFeeRateIx(this.context, params)
);
}
}

View File

@ -4,6 +4,9 @@ import WhirlpoolIDL from "./artifacts/whirlpool.json";
import { Whirlpool } from "./artifacts/whirlpool";
import { Wallet } from "@project-serum/anchor/dist/cjs/provider";
/**
* @category Core
*/
export class WhirlpoolContext {
readonly connection: Connection;
readonly wallet: Wallet;

View File

@ -0,0 +1,129 @@
import { AddressUtil, deriveATA, TransactionBuilder } from "@orca-so/common-sdk";
import { Address } from "@project-serum/anchor";
import { WhirlpoolContext } from "../context";
import {
IncreaseLiquidityInput,
DecreaseLiquidityInput,
increaseLiquidityIx,
decreaseLiquidityIx,
} from "../instructions";
import { PositionData } from "../types/public";
import { Position } from "../whirlpool-client";
import { PublicKey, Signer } from "@solana/web3.js";
import { AccountFetcher } from "../network/public";
import { PDAUtil, TickUtil } from "../utils/public";
import { toTx } from "../utils/instructions-util";
export class PositionImpl implements Position {
private data: PositionData;
constructor(
readonly ctx: WhirlpoolContext,
readonly fetcher: AccountFetcher,
readonly address: PublicKey,
data: PositionData
) {
this.data = data;
}
getData(): PositionData {
return this.data;
}
async refreshData() {
await this.refresh();
return this.data;
}
async increaseLiquidity(
liquidityInput: IncreaseLiquidityInput,
sourceWallet?: Address,
positionWallet?: Address
) {
const sourceWalletKey = sourceWallet
? AddressUtil.toPubKey(sourceWallet)
: this.ctx.wallet.publicKey;
const positionWalletKey = positionWallet
? AddressUtil.toPubKey(positionWallet)
: this.ctx.wallet.publicKey;
const whirlpool = await this.fetcher.getPool(this.data.whirlpool, true);
if (!whirlpool) {
throw new Error("Unable to fetch whirlpool for this position.");
}
return toTx(
this.ctx,
increaseLiquidityIx(this.ctx.program, {
...liquidityInput,
whirlpool: this.data.whirlpool,
position: this.address,
positionTokenAccount: await deriveATA(positionWalletKey, this.data.positionMint),
tokenOwnerAccountA: await deriveATA(sourceWalletKey, whirlpool.tokenMintA),
tokenOwnerAccountB: await deriveATA(sourceWalletKey, whirlpool.tokenMintB),
tokenVaultA: whirlpool.tokenVaultA,
tokenVaultB: whirlpool.tokenVaultB,
tickArrayLower: PDAUtil.getTickArray(
this.ctx.program.programId,
this.data.whirlpool,
TickUtil.getStartTickIndex(this.data.tickLowerIndex, whirlpool.tickSpacing)
).publicKey,
tickArrayUpper: PDAUtil.getTickArray(
this.ctx.program.programId,
this.data.whirlpool,
TickUtil.getStartTickIndex(this.data.tickUpperIndex, whirlpool.tickSpacing)
).publicKey,
positionAuthority: positionWalletKey,
})
);
}
async decreaseLiquidity(
liquidityInput: DecreaseLiquidityInput,
sourceWallet?: Address,
positionWallet?: Address
) {
const sourceWalletKey = sourceWallet
? AddressUtil.toPubKey(sourceWallet)
: this.ctx.wallet.publicKey;
const positionWalletKey = positionWallet
? AddressUtil.toPubKey(positionWallet)
: this.ctx.wallet.publicKey;
const whirlpool = await this.fetcher.getPool(this.data.whirlpool, true);
if (!whirlpool) {
throw new Error("Unable to fetch whirlpool for this position.");
}
return toTx(
this.ctx,
decreaseLiquidityIx(this.ctx.program, {
...liquidityInput,
whirlpool: this.data.whirlpool,
position: this.address,
positionTokenAccount: await deriveATA(positionWalletKey, this.data.positionMint),
tokenOwnerAccountA: await deriveATA(sourceWalletKey, whirlpool.tokenMintA),
tokenOwnerAccountB: await deriveATA(sourceWalletKey, whirlpool.tokenMintB),
tokenVaultA: whirlpool.tokenVaultA,
tokenVaultB: whirlpool.tokenVaultB,
tickArrayLower: PDAUtil.getTickArray(
this.ctx.program.programId,
this.data.whirlpool,
TickUtil.getStartTickIndex(this.data.tickLowerIndex, whirlpool.tickSpacing)
).publicKey,
tickArrayUpper: PDAUtil.getTickArray(
this.ctx.program.programId,
this.data.whirlpool,
TickUtil.getStartTickIndex(this.data.tickUpperIndex, whirlpool.tickSpacing)
).publicKey,
positionAuthority: positionWalletKey,
})
);
}
private async refresh() {
const account = await this.fetcher.getPosition(this.address, true);
if (!!account) {
this.data = account;
}
}
}

View File

@ -0,0 +1,53 @@
import { AddressUtil } from "@orca-so/common-sdk";
import { Address } from "@project-serum/anchor";
import { WhirlpoolContext } from "../context";
import { AccountFetcher } from "../network/public";
import { WhirlpoolData, TokenInfo } from "../types/public";
import { WhirlpoolClient, Whirlpool, Position } from "../whirlpool-client";
import { PositionImpl } from "./position-impl";
import { WhirlpoolImpl } from "./whirlpool-impl";
export class WhirlpoolClientImpl implements WhirlpoolClient {
constructor(readonly ctx: WhirlpoolContext, readonly fetcher: AccountFetcher) {}
public async getPool(poolAddress: Address): Promise<Whirlpool> {
const account = await this.fetcher.getPool(poolAddress, true);
if (!account) {
throw new Error(`Unable to fetch Whirlpool at address at ${poolAddress}`);
}
const tokenInfos = await getTokenInfos(this.fetcher, account);
return new WhirlpoolImpl(
this.ctx,
this.fetcher,
AddressUtil.toPubKey(poolAddress),
tokenInfos[0],
tokenInfos[1],
account
);
}
public async getPosition(positionAddress: Address): Promise<Position> {
const account = await this.fetcher.getPosition(positionAddress, true);
if (!account) {
throw new Error(`Unable to fetch Position at address at ${positionAddress}`);
}
return new PositionImpl(this.ctx, this.fetcher, AddressUtil.toPubKey(positionAddress), account);
}
}
async function getTokenInfos(fetcher: AccountFetcher, data: WhirlpoolData): Promise<TokenInfo[]> {
const mintA = data.tokenMintA;
const infoA = await fetcher.getMintInfo(mintA);
if (!infoA) {
throw new Error(`Unable to fetch MintInfo for mint - ${mintA}`);
}
const mintB = data.tokenMintB;
const infoB = await fetcher.getMintInfo(mintA);
if (!infoB) {
throw new Error(`Unable to fetch MintInfo for mint - ${mintB}`);
}
return [
{ mint: mintA, ...infoA },
{ mint: mintB, ...infoB },
];
}

View File

@ -0,0 +1,517 @@
import {
AddressUtil,
deriveATA,
Percentage,
resolveOrCreateATA,
TransactionBuilder,
ZERO,
} from "@orca-so/common-sdk";
import { Address, BN, translateAddress } from "@project-serum/anchor";
import { WhirlpoolContext } from "../context";
import {
IncreaseLiquidityInput,
openPositionIx,
openPositionWithMetadataIx,
initTickArrayIx,
increaseLiquidityIx,
decreaseLiquidityIx,
closePositionIx,
swapIx,
} from "../instructions";
import { MAX_SQRT_PRICE, MIN_SQRT_PRICE, TokenInfo, WhirlpoolData } from "../types/public";
import { Whirlpool } from "../whirlpool-client";
import { PublicKey, Keypair } from "@solana/web3.js";
import { u64 } from "@solana/spl-token";
import { AccountFetcher } from "../network/public";
import invariant from "tiny-invariant";
import { PDAUtil, PriceMath, TickUtil } from "../utils/public";
import { decreaseLiquidityQuoteByLiquidityWithParams, SwapQuote } from "../quotes/public";
export class WhirlpoolImpl implements Whirlpool {
private data: WhirlpoolData;
constructor(
readonly ctx: WhirlpoolContext,
readonly fetcher: AccountFetcher,
readonly address: PublicKey,
readonly tokenAInfo: TokenInfo,
readonly tokenBInfo: TokenInfo,
data: WhirlpoolData
) {
this.data = data;
}
getData(): WhirlpoolData {
return this.data;
}
getTokenAInfo(): TokenInfo {
return this.tokenAInfo;
}
getTokenBInfo(): TokenInfo {
return this.tokenBInfo;
}
async refreshData() {
await this.refresh();
return this.data;
}
async openPosition(
tickLower: number,
tickUpper: number,
liquidityInput: IncreaseLiquidityInput,
sourceWallet?: Address,
positionWallet?: Address,
funder?: Address
) {
await this.refresh();
return this.getOpenPositionWithOptMetadataTx(
tickLower,
tickUpper,
liquidityInput,
!!sourceWallet ? AddressUtil.toPubKey(sourceWallet) : this.ctx.wallet.publicKey,
!!positionWallet ? AddressUtil.toPubKey(positionWallet) : this.ctx.wallet.publicKey,
!!funder ? AddressUtil.toPubKey(funder) : this.ctx.wallet.publicKey
);
}
async openPositionWithMetadata(
tickLower: number,
tickUpper: number,
liquidityInput: IncreaseLiquidityInput,
sourceWallet?: Address,
positionWallet?: Address,
funder?: Address
) {
await this.refresh();
return this.getOpenPositionWithOptMetadataTx(
tickLower,
tickUpper,
liquidityInput,
!!sourceWallet ? AddressUtil.toPubKey(sourceWallet) : this.ctx.wallet.publicKey,
!!positionWallet ? AddressUtil.toPubKey(positionWallet) : this.ctx.wallet.publicKey,
!!funder ? AddressUtil.toPubKey(funder) : this.ctx.wallet.publicKey,
true
);
}
initTickArrayForTicks(ticks: number[], funder?: Address) {
const startTicks = ticks.map((tick) => TickUtil.getStartTickIndex(tick, this.data.tickSpacing));
const tx = new TransactionBuilder(this.ctx.provider);
const initializedArrayTicks: number[] = [];
startTicks.forEach((startTick) => {
if (initializedArrayTicks.includes(startTick)) {
return;
}
initializedArrayTicks.push(startTick);
const tickArrayPda = PDAUtil.getTickArray(
this.ctx.program.programId,
this.address,
startTick
);
tx.addInstruction(
initTickArrayIx(this.ctx.program, {
startTick,
tickArrayPda,
whirlpool: this.address,
funder: !!funder ? AddressUtil.toPubKey(funder) : this.ctx.wallet.publicKey,
})
);
});
return tx;
}
async closePosition(
positionAddress: Address,
slippageTolerance: Percentage,
destinationWallet?: Address,
positionWallet?: Address
) {
await this.refresh();
const positionWalletKey = positionWallet
? AddressUtil.toPubKey(positionWallet)
: this.ctx.wallet.publicKey;
const destinationWalletKey = destinationWallet
? AddressUtil.toPubKey(destinationWallet)
: this.ctx.wallet.publicKey;
return this.getClosePositionIx(
AddressUtil.toPubKey(positionAddress),
slippageTolerance,
destinationWalletKey,
positionWalletKey
);
}
async swap(quote: SwapQuote, sourceWallet?: Address) {
const sourceWalletKey = sourceWallet
? AddressUtil.toPubKey(sourceWallet)
: this.ctx.wallet.publicKey;
return this.getSwapTx(quote, sourceWalletKey);
}
/**
* Construct a transaction for opening an new position with optional metadata
*/
async getOpenPositionWithOptMetadataTx(
tickLower: number,
tickUpper: number,
liquidityInput: IncreaseLiquidityInput,
sourceWallet: PublicKey,
positionWallet: PublicKey,
funder: PublicKey,
withMetadata: boolean = false
): Promise<{ positionMint: PublicKey; tx: TransactionBuilder }> {
invariant(TickUtil.checkTickInBounds(tickLower), "tickLower is out of bounds.");
invariant(TickUtil.checkTickInBounds(tickUpper), "tickUpper is out of bounds.");
const { liquidityAmount: liquidity, tokenMaxA, tokenMaxB } = liquidityInput;
invariant(liquidity.gt(new u64(0)), "liquidity must be greater than zero");
const whirlpool = await this.fetcher.getPool(this.address, false);
if (!whirlpool) {
throw new Error(`Whirlpool not found: ${translateAddress(this.address).toBase58()}`);
}
invariant(
TickUtil.isTickInitializable(tickLower, whirlpool.tickSpacing),
`lower tick ${tickLower} is not an initializable tick for tick-spacing ${whirlpool.tickSpacing}`
);
invariant(
TickUtil.isTickInitializable(tickUpper, whirlpool.tickSpacing),
`upper tick ${tickUpper} is not an initializable tick for tick-spacing ${whirlpool.tickSpacing}`
);
const positionMintKeypair = Keypair.generate();
const positionPda = PDAUtil.getPosition(
this.ctx.program.programId,
positionMintKeypair.publicKey
);
const metadataPda = PDAUtil.getPositionMetadata(positionMintKeypair.publicKey);
const positionTokenAccountAddress = await deriveATA(
positionWallet,
positionMintKeypair.publicKey
);
const txBuilder = new TransactionBuilder(this.ctx.provider);
const positionIx = (withMetadata ? openPositionWithMetadataIx : openPositionIx)(
this.ctx.program,
{
funder,
owner: positionWallet,
positionPda,
metadataPda,
positionMintAddress: positionMintKeypair.publicKey,
positionTokenAccount: positionTokenAccountAddress,
whirlpool: this.address,
tickLowerIndex: tickLower,
tickUpperIndex: tickUpper,
}
);
txBuilder.addInstruction(positionIx).addSigner(positionMintKeypair);
const { address: tokenOwnerAccountA, ...tokenOwnerAccountAIx } = await resolveOrCreateATA(
this.ctx.connection,
sourceWallet,
whirlpool.tokenMintA,
() => this.fetcher.getAccountRentExempt(),
tokenMaxA
);
const { address: tokenOwnerAccountB, ...tokenOwnerAccountBIx } = await resolveOrCreateATA(
this.ctx.connection,
sourceWallet,
whirlpool.tokenMintB,
() => this.fetcher.getAccountRentExempt(),
tokenMaxB
);
txBuilder.addInstruction(tokenOwnerAccountAIx);
txBuilder.addInstruction(tokenOwnerAccountBIx);
const tickArrayLowerPda = PDAUtil.getTickArrayFromTickIndex(
tickLower,
this.data.tickSpacing,
this.address,
this.ctx.program.programId
);
const tickArrayUpperPda = PDAUtil.getTickArrayFromTickIndex(
tickUpper,
this.data.tickSpacing,
this.address,
this.ctx.program.programId
);
const [tickArrayLower, tickArrayUpper] = await this.fetcher.listTickArrays(
[tickArrayLowerPda.publicKey, tickArrayUpperPda.publicKey],
true
);
invariant(!!tickArrayLower, "tickArray for the tickLower has not been initialized");
invariant(!!tickArrayUpper, "tickArray for the tickUpper has not been initialized");
const liquidityIx = increaseLiquidityIx(this.ctx.program, {
liquidityAmount: liquidity,
tokenMaxA,
tokenMaxB,
whirlpool: this.address,
positionAuthority: positionWallet,
position: positionPda.publicKey,
positionTokenAccount: positionTokenAccountAddress,
tokenOwnerAccountA,
tokenOwnerAccountB,
tokenVaultA: whirlpool.tokenVaultA,
tokenVaultB: whirlpool.tokenVaultB,
tickArrayLower: tickArrayLowerPda.publicKey,
tickArrayUpper: tickArrayUpperPda.publicKey,
});
txBuilder.addInstruction(liquidityIx);
return {
positionMint: positionMintKeypair.publicKey,
tx: txBuilder,
};
}
async getClosePositionIx(
positionAddress: PublicKey,
slippageTolerance: Percentage,
destinationWallet: PublicKey,
positionWallet: PublicKey
): Promise<TransactionBuilder> {
const position = await this.fetcher.getPosition(positionAddress, true);
if (!position) {
throw new Error(`Position not found: ${positionAddress.toBase58()}`);
}
const whirlpool = this.data;
invariant(
position.whirlpool.equals(this.address),
`Position ${positionAddress.toBase58()} is not a position for Whirlpool ${this.address.toBase58()}`
);
const tickArrayLower = PDAUtil.getTickArrayFromTickIndex(
position.tickLowerIndex,
whirlpool.tickSpacing,
position.whirlpool,
this.ctx.program.programId
).publicKey;
const tickArrayUpper = PDAUtil.getTickArrayFromTickIndex(
position.tickUpperIndex,
whirlpool.tickSpacing,
position.whirlpool,
this.ctx.program.programId
).publicKey;
const positionTokenAccount = await deriveATA(positionWallet, position.positionMint);
const txBuilder = new TransactionBuilder(this.ctx.provider);
const resolvedAssociatedTokenAddresses: Record<string, PublicKey> = {};
const { address: tokenOwnerAccountA, ...createTokenOwnerAccountAIx } = await resolveOrCreateATA(
this.ctx.connection,
destinationWallet,
whirlpool.tokenMintA,
() => this.fetcher.getAccountRentExempt()
);
const { address: tokenOwnerAccountB, ...createTokenOwnerAccountBIx } = await resolveOrCreateATA(
this.ctx.connection,
destinationWallet,
whirlpool.tokenMintB,
() => this.fetcher.getAccountRentExempt()
);
txBuilder.addInstruction(createTokenOwnerAccountAIx).addInstruction(createTokenOwnerAccountBIx);
resolvedAssociatedTokenAddresses[whirlpool.tokenMintA.toBase58()] = tokenOwnerAccountA;
resolvedAssociatedTokenAddresses[whirlpool.tokenMintB.toBase58()] = tokenOwnerAccountB;
// TODO: Collect all Fees and rewards for the position.
// TODO: Optimize - no need to call updateFee if we call decreaseLiquidity first.
// const collectTx = await buildCollectFeesAndRewardsTx(this.dal, {
// provider,
// positionAddress: positionAddress,
// resolvedAssociatedTokenAddresses,
// });
// txBuilder.addInstruction(collectTx.compressIx(false));
/* Remove all liquidity remaining in the position */
if (position.liquidity.gt(new u64(0))) {
const decreaseLiqQuote = decreaseLiquidityQuoteByLiquidityWithParams({
liquidity: position.liquidity,
slippageTolerance,
sqrtPrice: whirlpool.sqrtPrice,
tickCurrentIndex: whirlpool.tickCurrentIndex,
tickLowerIndex: position.tickLowerIndex,
tickUpperIndex: position.tickUpperIndex,
});
const liquidityIx = decreaseLiquidityIx(this.ctx.program, {
liquidityAmount: decreaseLiqQuote.liquidityAmount,
tokenMinA: decreaseLiqQuote.tokenMinA,
tokenMinB: decreaseLiqQuote.tokenMinB,
whirlpool: position.whirlpool,
positionAuthority: positionWallet,
position: positionAddress,
positionTokenAccount,
tokenOwnerAccountA,
tokenOwnerAccountB,
tokenVaultA: whirlpool.tokenVaultA,
tokenVaultB: whirlpool.tokenVaultB,
tickArrayLower,
tickArrayUpper,
});
txBuilder.addInstruction(liquidityIx);
}
/* Close position */
const positionIx = closePositionIx(this.ctx.program, {
positionAuthority: this.ctx.wallet.publicKey,
receiver: this.ctx.wallet.publicKey,
positionTokenAccount,
position: positionAddress,
positionMint: position.positionMint,
});
txBuilder.addInstruction(positionIx);
return txBuilder;
}
private async getSwapTx(quote: SwapQuote, wallet: PublicKey): Promise<TransactionBuilder> {
const {
sqrtPriceLimit,
otherAmountThreshold,
estimatedAmountIn,
estimatedAmountOut,
aToB,
amountSpecifiedIsInput,
} = quote;
const whirlpool = this.data;
const txBuilder = new TransactionBuilder(this.ctx.provider);
const { address: tokenOwnerAccountA, ...tokenOwnerAccountAIx } = await resolveOrCreateATA(
this.ctx.connection,
wallet,
whirlpool.tokenMintA,
() => this.fetcher.getAccountRentExempt(),
aToB ? estimatedAmountIn : ZERO
);
txBuilder.addInstruction(tokenOwnerAccountAIx);
const { address: tokenOwnerAccountB, ...tokenOwnerAccountBIx } = await resolveOrCreateATA(
this.ctx.connection,
wallet,
whirlpool.tokenMintB,
() => this.fetcher.getAccountRentExempt(),
!aToB ? estimatedAmountIn : ZERO
);
txBuilder.addInstruction(tokenOwnerAccountBIx);
const targetSqrtPriceLimitX64 = sqrtPriceLimit || this.getDefaultSqrtPriceLimit(aToB);
const tickArrayAddresses = await this.getTickArrayPublicKeysForSwap(
whirlpool.tickCurrentIndex,
targetSqrtPriceLimitX64,
whirlpool.tickSpacing,
this.address,
this.ctx.program.programId,
aToB
);
const oraclePda = PDAUtil.getOracle(this.ctx.program.programId, this.address);
txBuilder.addInstruction(
swapIx(this.ctx.program, {
amount: amountSpecifiedIsInput ? estimatedAmountIn : estimatedAmountOut,
otherAmountThreshold,
sqrtPriceLimit: targetSqrtPriceLimitX64,
amountSpecifiedIsInput,
aToB,
whirlpool: this.address,
tokenAuthority: wallet,
tokenOwnerAccountA,
tokenVaultA: whirlpool.tokenVaultA,
tokenOwnerAccountB,
tokenVaultB: whirlpool.tokenVaultB,
tickArray0: tickArrayAddresses[0],
tickArray1: tickArrayAddresses[1],
tickArray2: tickArrayAddresses[2],
oracle: oraclePda.publicKey,
})
);
return txBuilder;
}
private async getTickArrayPublicKeysForSwap(
tickCurrentIndex: number,
targetSqrtPriceX64: BN,
tickSpacing: number,
poolAddress: PublicKey,
programId: PublicKey,
aToB: boolean
): Promise<[PublicKey, PublicKey, PublicKey]> {
// TODO: fix directionality
const nextInitializableTickIndex = (
aToB ? TickUtil.getPrevInitializableTickIndex : TickUtil.getNextInitializableTickIndex
)(tickCurrentIndex, tickSpacing);
const targetTickIndex = PriceMath.sqrtPriceX64ToTickIndex(targetSqrtPriceX64);
let currentStartTickIndex = TickUtil.getStartTickIndex(nextInitializableTickIndex, tickSpacing);
const targetStartTickIndex = TickUtil.getStartTickIndex(targetTickIndex, tickSpacing);
const offset = nextInitializableTickIndex < targetTickIndex ? 1 : -1;
let count = 1;
const tickArrayAddresses: [PublicKey, PublicKey, PublicKey] = [
PDAUtil.getTickArray(programId, poolAddress, currentStartTickIndex).publicKey,
PublicKey.default,
PublicKey.default,
];
while (currentStartTickIndex !== targetStartTickIndex && count < 3) {
const nextStartTickIndex = TickUtil.getStartTickIndex(
nextInitializableTickIndex,
tickSpacing,
offset * count
);
const nextTickArrayAddress = PDAUtil.getTickArray(
programId,
poolAddress,
nextStartTickIndex
).publicKey;
const nextTickArray = await this.fetcher.getTickArray(nextTickArrayAddress, false);
if (!nextTickArray) {
break;
}
tickArrayAddresses[count] = nextTickArrayAddress;
count++;
currentStartTickIndex = nextStartTickIndex;
}
while (count < 3) {
tickArrayAddresses[count] = PDAUtil.getTickArray(
programId,
poolAddress,
currentStartTickIndex
).publicKey;
count++;
}
return tickArrayAddresses;
}
private getDefaultSqrtPriceLimit(aToB: boolean): BN {
return new BN(aToB ? MIN_SQRT_PRICE : MAX_SQRT_PRICE);
}
private async refresh() {
const account = await this.fetcher.getPool(this.address, true);
if (!!account) {
this.data = account;
}
}
}

View File

@ -1,7 +1,17 @@
export * from "./client";
import Decimal from "decimal.js";
export * from "./context";
export * from "./types/public";
export * from "./utils/public";
export * from "./network/public";
export * from "./quotes/public";
export * from "./ix";
export * from "./whirlpool-client";
export * from "./types/public/anchor-types";
export * from "./utils/transactions/transactions-builder";
// Global rules for Decimals
// - 40 digits of precision for the largest number
// - 20 digits of precision for the smallest number
// - Always round towards 0 to mirror smart contract rules
Decimal.set({ precision: 40, toExpPos: 40, toExpNeg: -20, rounding: 1 });

View File

@ -1,15 +1,48 @@
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { ClosePositionParams } from "..";
import { WhirlpoolContext } from "../context";
import { Instruction } from "../utils/transactions/transactions-builder";
import { Instruction } from "@orca-so/common-sdk";
import { PublicKey } from "@solana/web3.js";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
export function buildClosePositionIx(
context: WhirlpoolContext,
/**
* Parameters to close a position in a Whirlpool.
*
* @category Instruction Types
* @param receiver - PublicKey for the wallet that will receive the rented lamports.
* @param position - PublicKey for the position.
* @param positionMint - PublicKey for the mint token for the Position token.
* @param positionTokenAccount - The associated token address for the position token in the owners wallet.
* @param positionAuthority - Authority that owns the position token.
*/
export type ClosePositionParams = {
receiver: PublicKey;
position: PublicKey;
positionMint: PublicKey;
positionTokenAccount: PublicKey;
positionAuthority: PublicKey;
};
/**
* Close a position in a Whirlpool. Burns the position token in the owner's wallet.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - ClosePositionParams object
* @returns - Instruction to perform the action.
*/
export function closePositionIx(
program: Program<Whirlpool>,
params: ClosePositionParams
): Instruction {
const { positionAuthority, receiver, position, positionMint, positionTokenAccount } = params;
const {
positionAuthority,
receiver: receiver,
position: position,
positionMint: positionMint,
positionTokenAccount,
} = params;
const ix = context.program.instruction.closePosition({
const ix = program.instruction.closePosition({
accounts: {
positionAuthority,
receiver,

View File

@ -1,12 +1,44 @@
import { WhirlpoolContext } from "../context";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { Instruction } from "../utils/transactions/transactions-builder";
import { CollectFeesParams } from "..";
import { PublicKey } from "@solana/web3.js";
export function buildCollectFeesIx(
context: WhirlpoolContext,
params: CollectFeesParams
): Instruction {
import { Instruction } from "@orca-so/common-sdk";
/**
* Parameters to collect fees from a position.
*
* @category Instruction Types
* @param whirlpool - PublicKey for the whirlpool that the position will be opened for.
* @param position - PublicKey for the position will be opened for.
* @param positionTokenAccount - PublicKey for the position token's associated token address.
* @param tokenOwnerAccountA - PublicKey for the token A account that will be withdrawed from.
* @param tokenOwnerAccountB - PublicKey for the token B account that will be withdrawed from.
* @param tokenVaultA - PublicKey for the tokenA vault for this whirlpool.
* @param tokenVaultB - PublicKey for the tokenB vault for this whirlpool.
* @param positionAuthority - authority that owns the token corresponding to this desired position.
*/
export type CollectFeesParams = {
whirlpool: PublicKey;
position: PublicKey;
positionTokenAccount: PublicKey;
tokenOwnerAccountA: PublicKey;
tokenOwnerAccountB: PublicKey;
tokenVaultA: PublicKey;
tokenVaultB: PublicKey;
positionAuthority: PublicKey;
};
/**
* Collect fees accrued for this position.
* Call updateFeesAndRewards before this to update the position to the newest accrued values.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - CollectFeesParams object
* @returns - Instruction to perform the action.
*/
export function collectFeesIx(program: Program<Whirlpool>, params: CollectFeesParams): Instruction {
const {
whirlpool,
positionAuthority,
@ -18,7 +50,7 @@ export function buildCollectFeesIx(
tokenVaultB,
} = params;
const ix = context.program.instruction.collectFees({
const ix = program.instruction.collectFees({
accounts: {
whirlpool,
positionAuthority,

View File

@ -1,10 +1,41 @@
import { WhirlpoolContext } from "../context";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { Instruction } from "../utils/transactions/transactions-builder";
import { CollectProtocolFeesParams } from "..";
import { Instruction } from "@orca-so/common-sdk";
import { PublicKey } from "@solana/web3.js";
export function buildCollectProtocolFeesIx(
context: WhirlpoolContext,
/**
* Parameters to collect protocol fees for a Whirlpool
*
* @category Instruction Types
* @param whirlpoolsConfig - The public key for the WhirlpoolsConfig this pool is initialized in
* @param whirlpool - PublicKey for the whirlpool that the position will be opened for.
* @param tokenVaultA - PublicKey for the tokenA vault for this whirlpool.
* @param tokenVaultB - PublicKey for the tokenB vault for this whirlpool.
* @param tokenOwnerAccountA - PublicKey for the associated token account for tokenA in the collection wallet
* @param tokenOwnerAccountB - PublicKey for the associated token account for tokenA in the collection wallet
* @param collectProtocolFeesAuthority - assigned authority in the WhirlpoolsConfig that can collect protocol fees
*/
export type CollectProtocolFeesParams = {
whirlpoolsConfig: PublicKey;
whirlpool: PublicKey;
tokenVaultA: PublicKey;
tokenVaultB: PublicKey;
tokenOwnerAccountA: PublicKey;
tokenOwnerAccountB: PublicKey;
collectProtocolFeesAuthority: PublicKey;
};
/**
* Collect protocol fees accrued in this Whirlpool.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - CollectProtocolFeesParams object
* @returns - Instruction to perform the action.
*/
export function collectProtocolFeesIx(
program: Program<Whirlpool>,
params: CollectProtocolFeesParams
): Instruction {
const {
@ -13,11 +44,11 @@ export function buildCollectProtocolFeesIx(
collectProtocolFeesAuthority,
tokenVaultA,
tokenVaultB,
tokenDestinationA,
tokenDestinationB,
tokenOwnerAccountA: tokenDestinationA,
tokenOwnerAccountB: tokenDestinationB,
} = params;
const ix = context.program.instruction.collectProtocolFees({
const ix = program.instruction.collectProtocolFees({
accounts: {
whirlpoolsConfig,
whirlpool,

View File

@ -1,10 +1,42 @@
import { WhirlpoolContext } from "../context";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { Instruction } from "../utils/transactions/transactions-builder";
import { CollectRewardParams } from "..";
import { Instruction } from "@orca-so/common-sdk";
import { PublicKey } from "@solana/web3.js";
export function buildCollectRewardIx(
context: WhirlpoolContext,
/**
* Parameters to collect rewards from a reward index in a position.
*
* @category Instruction Types
* @param whirlpool - PublicKey for the whirlpool that the position will be opened for.
* @param position - PublicKey for the position will be opened for.
* @param positionTokenAccount - PublicKey for the position token's associated token address.
* @param rewardIndex - The reward index that we'd like to initialize. (0 <= index <= NUM_REWARDS).
* @param rewardOwnerAccount - PublicKey for the reward token account that the reward will deposit into.
* @param rewardVault - PublicKey of the vault account that reward will be withdrawn from.
* @param positionAuthority - authority that owns the token corresponding to this desired position.
*/
export type CollectRewardParams = {
whirlpool: PublicKey;
position: PublicKey;
positionTokenAccount: PublicKey;
rewardIndex: number;
rewardOwnerAccount: PublicKey;
rewardVault: PublicKey;
positionAuthority: PublicKey;
};
/**
* Collect rewards accrued for this reward index in a position.
* Call updateFeesAndRewards before this to update the position to the newest accrued values.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - CollectRewardParams object
* @returns - Instruction to perform the action.
*/
export function collectRewardIx(
program: Program<Whirlpool>,
params: CollectRewardParams
): Instruction {
const {
@ -17,7 +49,7 @@ export function buildCollectRewardIx(
rewardIndex,
} = params;
const ix = context.program.instruction.collectReward(rewardIndex, {
const ix = program.instruction.collectReward(rewardIndex, {
accounts: {
whirlpool,
positionAuthority,

View File

@ -1,10 +1,65 @@
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { DecreaseLiquidityParams } from "..";
import { WhirlpoolContext } from "../context";
import { Instruction } from "../utils/transactions/transactions-builder";
import { TOKEN_PROGRAM_ID, u64 } from "@solana/spl-token";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
import { Instruction } from "@orca-so/common-sdk";
import { PublicKey } from "@solana/web3.js";
import { BN } from "@project-serum/anchor";
export function buildDecreaseLiquidityIx(
context: WhirlpoolContext,
/**
* Parameters to remove liquidity from a position.
*
* @category Instruction Types
* @param liquidityAmount - The total amount of Liquidity the user is withdrawing
* @param tokenMinA - The minimum amount of token A to remove from the position.
* @param tokenMinB - The minimum amount of token B to remove from the position.
* @param whirlpool - PublicKey for the whirlpool that the position will be opened for.
* @param position - PublicKey for the position will be opened for.
* @param positionTokenAccount - PublicKey for the position token's associated token address.
* @param tokenOwnerAccountA - PublicKey for the token A account that will be withdrawed from.
* @param tokenOwnerAccountB - PublicKey for the token B account that will be withdrawed from.
* @param tokenVaultA - PublicKey for the tokenA vault for this whirlpool.
* @param tokenVaultB - PublicKey for the tokenB vault for this whirlpool.
* @param tickArrayLower - PublicKey for the tick-array account that hosts the tick at the lower tick index.
* @param tickArrayUpper - PublicKey for the tick-array account that hosts the tick at the upper tick index.
* @param positionAuthority - authority that owns the token corresponding to this desired position.
*/
export type DecreaseLiquidityParams = {
whirlpool: PublicKey;
position: PublicKey;
positionTokenAccount: PublicKey;
tokenOwnerAccountA: PublicKey;
tokenOwnerAccountB: PublicKey;
tokenVaultA: PublicKey;
tokenVaultB: PublicKey;
tickArrayLower: PublicKey;
tickArrayUpper: PublicKey;
positionAuthority: PublicKey;
} & DecreaseLiquidityInput;
/**
* @category Instruction Types
*/
export type DecreaseLiquidityInput = {
tokenMinA: BN;
tokenMinB: BN;
liquidityAmount: BN;
};
/**
* Remove liquidity to a position in the Whirlpool.
*
* #### Special Errors
* - `LiquidityZero` - Provided liquidity amount is zero.
* - `LiquidityTooHigh` - Provided liquidity exceeds u128::max.
* - `TokenMinSubceeded` - The required token to perform this operation subceeds the user defined amount.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - DecreaseLiquidityParams object
* @returns - Instruction to perform the action.
*/
export function decreaseLiquidityIx(
program: Program<Whirlpool>,
params: DecreaseLiquidityParams
): Instruction {
const {
@ -23,7 +78,7 @@ export function buildDecreaseLiquidityIx(
tickArrayUpper,
} = params;
const ix = context.program.instruction.decreaseLiquidity(liquidityAmount, tokenMinA, tokenMinB, {
const ix = program.instruction.decreaseLiquidity(liquidityAmount, tokenMinA, tokenMinB, {
accounts: {
whirlpool,
tokenProgram: TOKEN_PROGRAM_ID,

View File

@ -1,10 +1,73 @@
import { WhirlpoolContext } from "../context";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { Instruction } from "../utils/transactions/transactions-builder";
import { IncreaseLiquidityParams } from "..";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
import { TOKEN_PROGRAM_ID, u64 } from "@solana/spl-token";
import { PublicKey } from "@solana/web3.js";
export function buildIncreaseLiquidityIx(
context: WhirlpoolContext,
import { Instruction } from "@orca-so/common-sdk";
/**
* Parameters to increase liquidity for a position.
*
* @category Instruction Types
* @param liquidityAmount - The total amount of Liquidity the user is willing to deposit.
* @param tokenMaxA - The maximum amount of token A to add to the position.
* @param tokenMaxB - The maximum amount of token B to add to the position.
* @param whirlpool - PublicKey for the whirlpool that the position will be opened for.
* @param position - PublicKey for the position will be opened for.
* @param positionTokenAccount - PublicKey for the position token's associated token address.
* @param tokenOwnerAccountA - PublicKey for the token A account that will be withdrawed from.
* @param tokenOwnerAccountB - PublicKey for the token B account that will be withdrawed from.
* @param tokenVaultA - PublicKey for the tokenA vault for this whirlpool.
* @param tokenVaultB - PublicKey for the tokenB vault for this whirlpool.
* @param tickArrayLower - PublicKey for the tick-array account that hosts the tick at the lower tick index.
* @param tickArrayUpper - PublicKey for the tick-array account that hosts the tick at the upper tick index.
* @param positionAuthority - authority that owns the token corresponding to this desired position.
*/
export type IncreaseLiquidityParams = {
whirlpool: PublicKey;
position: PublicKey;
positionTokenAccount: PublicKey;
tokenOwnerAccountA: PublicKey;
tokenOwnerAccountB: PublicKey;
tokenVaultA: PublicKey;
tokenVaultB: PublicKey;
tickArrayLower: PublicKey;
tickArrayUpper: PublicKey;
positionAuthority: PublicKey;
} & IncreaseLiquidityInput;
/**
* Input parameters to deposit liquidity into a position.
*
* This type is usually generated by a quote class to estimate the amount of tokens required to
* deposit a certain amount of liquidity into a position.
*
* @category Instruction Types
* @param tokenMaxA - the maximum amount of tokenA allowed to withdraw from the source wallet.
* @param tokenMaxB - the maximum amount of tokenB allowed to withdraw from the source wallet.
* @param liquidityAmount - the desired amount of liquidity to deposit into the position/
*/
export type IncreaseLiquidityInput = {
tokenMaxA: u64;
tokenMaxB: u64;
liquidityAmount: u64;
};
/**
* Add liquidity to a position in the Whirlpool.
*
* #### Special Errors
* `LiquidityZero` - Provided liquidity amount is zero.
* `LiquidityTooHigh` - Provided liquidity exceeds u128::max.
* `TokenMaxExceeded` - The required token to perform this operation exceeds the user defined amount.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - IncreaseLiquidityParams object
* @returns - Instruction to perform the action.
*/
export function increaseLiquidityIx(
program: Program<Whirlpool>,
params: IncreaseLiquidityParams
): Instruction {
const {
@ -23,7 +86,7 @@ export function buildIncreaseLiquidityIx(
tickArrayUpper,
} = params;
const ix = context.program.instruction.increaseLiquidity(liquidityAmount, tokenMaxA, tokenMaxB, {
const ix = program.instruction.increaseLiquidity(liquidityAmount, tokenMaxA, tokenMaxB, {
accounts: {
whirlpool,
tokenProgram: TOKEN_PROGRAM_ID,

View File

@ -0,0 +1,24 @@
export * from "./close-position-ix";
export * from "./collect-fees-ix";
export * from "./collect-protocol-fees-ix";
export * from "./collect-reward-ix";
export * from "./decrease-liquidity-ix";
export * from "./increase-liquidity-ix";
export * from "./initialize-config-ix";
export * from "./initialize-fee-tier-ix";
export * from "./initialize-pool-ix";
export * from "./initialize-reward-ix";
export * from "./initialize-tick-array-ix";
export * from "./open-position-ix";
export * from "./set-collect-protocol-fees-authority-ix";
export * from "./set-default-fee-rate-ix";
export * from "./set-default-protocol-fee-rate-ix";
export * from "./set-fee-authority-ix";
export * from "./set-fee-rate-ix";
export * from "./set-protocol-fee-rate-ix";
export * from "./set-reward-authority-by-super-authority-ix";
export * from "./set-reward-authority-ix";
export * from "./set-reward-emissions-ix";
export * from "./set-reward-emissions-super-authority-ix";
export * from "./swap-ix";
export * from "./update-fees-and-rewards-ix";

View File

@ -1,10 +1,40 @@
import { SystemProgram } from "@solana/web3.js";
import { WhirlpoolContext } from "../context";
import { InitConfigParams } from "../types/public/ix-types";
import { Instruction } from "../utils/transactions/transactions-builder";
import { SystemProgram, Keypair, PublicKey } from "@solana/web3.js";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
export function buildInitializeConfigIx(
context: WhirlpoolContext,
import { Instruction } from "@orca-so/common-sdk";
/**
* Parameters to initialize a WhirlpoolsConfig account.
*
* @category Instruction Types
* @param whirlpoolsConfigKeypair - Generated keypair for the WhirlpoolsConfig.
* @param feeAuthority - Authority authorized to initialize fee-tiers and set customs fees.
* @param collect_protocol_fees_authority - Authority authorized to collect protocol fees.
* @param rewardEmissionsSuperAuthority - Authority authorized to set reward authorities in pools.
* @param defaultProtocolFeeRate - The default protocol fee rate. Stored as a basis point of the total fees collected by feeRate.
* @param funder - The account that would fund the creation of this account
*/
export type InitConfigParams = {
whirlpoolsConfigKeypair: Keypair;
feeAuthority: PublicKey;
collectProtocolFeesAuthority: PublicKey;
rewardEmissionsSuperAuthority: PublicKey;
defaultProtocolFeeRate: number;
funder: PublicKey;
};
/**
* Initializes a WhirlpoolsConfig account that hosts info & authorities
* required to govern a set of Whirlpools.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - InitConfigParams object
* @returns - Instruction to perform the action.
*/
export function initializeConfigIx(
program: Program<Whirlpool>,
params: InitConfigParams
): Instruction {
const {
@ -15,14 +45,14 @@ export function buildInitializeConfigIx(
funder,
} = params;
const ix = context.program.instruction.initializeConfig(
const ix = program.instruction.initializeConfig(
feeAuthority,
collectProtocolFeesAuthority,
rewardEmissionsSuperAuthority,
defaultProtocolFeeRate,
{
accounts: {
config: params.whirlpoolConfigKeypair.publicKey,
config: params.whirlpoolsConfigKeypair.publicKey,
funder,
systemProgram: SystemProgram.programId,
},
@ -32,6 +62,6 @@ export function buildInitializeConfigIx(
return {
instructions: [ix],
cleanupInstructions: [],
signers: [params.whirlpoolConfigKeypair],
signers: [params.whirlpoolsConfigKeypair],
};
}

View File

@ -0,0 +1,61 @@
import { SystemProgram, PublicKey } from "@solana/web3.js";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
import { PDA } from "@orca-so/common-sdk";
import { Instruction } from "@orca-so/common-sdk";
/**
* Parameters to initialize a FeeTier account.
*
* @category Instruction Types
* @param whirlpoolsConfig - PublicKey for the whirlpool config space that the fee-tier will be initialized for.
* @param feeTierPda - PDA for the fee-tier account that will be initialized
* @param tickSpacing - The tick spacing this fee tier recommends its default fee rate for.
* @param defaultFeeRate - The default fee rate for this fee-tier. Stored as a hundredths of a basis point.
* @param feeAuthority - Authority authorized to initialize fee-tiers and set customs fees.
* @param funder - The account that would fund the creation of this account
*/
export type InitFeeTierParams = {
whirlpoolsConfig: PublicKey;
feeTierPda: PDA;
tickSpacing: number;
defaultFeeRate: number;
feeAuthority: PublicKey;
funder: PublicKey;
};
/**
* Initializes a fee tier account usable by Whirlpools in this WhirlpoolsConfig space.
*
* Special Errors
* `FeeRateMaxExceeded` - If the provided default_fee_rate exceeds MAX_FEE_RATE.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - InitFeeTierParams object
* @returns - Instruction to perform the action.
*/
export function initializeFeeTierIx(
program: Program<Whirlpool>,
params: InitFeeTierParams
): Instruction {
const { feeTierPda, whirlpoolsConfig, tickSpacing, feeAuthority, defaultFeeRate, funder } =
params;
const ix = program.instruction.initializeFeeTier(tickSpacing, defaultFeeRate, {
accounts: {
config: whirlpoolsConfig,
feeTier: feeTierPda.publicKey,
feeAuthority,
funder,
systemProgram: SystemProgram.programId,
},
});
return {
instructions: [ix],
cleanupInstructions: [],
signers: [],
};
}

View File

@ -1,29 +0,0 @@
import { SystemProgram } from "@solana/web3.js";
import { getFeeTierPda } from "..";
import { WhirlpoolContext } from "../context";
import { InitFeeTierParams } from "../types/public/ix-types";
import { Instruction } from "../utils/transactions/transactions-builder";
export function buildInitializeFeeTier(
context: WhirlpoolContext,
params: InitFeeTierParams
): Instruction {
const { feeTierPda, whirlpoolConfigKey, tickSpacing, feeAuthority, defaultFeeRate, funder } =
params;
const ix = context.program.instruction.initializeFeeTier(tickSpacing, defaultFeeRate, {
accounts: {
config: whirlpoolConfigKey,
feeTier: feeTierPda.publicKey,
feeAuthority,
funder,
systemProgram: SystemProgram.programId,
},
});
return {
instructions: [ix],
cleanupInstructions: [],
signers: [],
};
}

View File

@ -1,18 +1,58 @@
import { WhirlpoolContext } from "../context";
import { SystemProgram, SYSVAR_RENT_PUBKEY } from "@solana/web3.js";
import { Instruction } from "../utils/transactions/transactions-builder";
import { InitPoolParams } from "..";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
import { SystemProgram, SYSVAR_RENT_PUBKEY, PublicKey, Keypair } from "@solana/web3.js";
import { Instruction } from "@orca-so/common-sdk";
import { WhirlpoolBumpsData } from "../types/public/anchor-types";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { BN } from "@project-serum/anchor";
import { PDA } from "@orca-so/common-sdk";
export function buildInitPoolIx(context: WhirlpoolContext, params: InitPoolParams): Instruction {
const program = context.program;
/**
* Parameters to initialize a Whirlpool account.
*
* @category Instruction Types
* @param initSqrtPrice - The desired initial sqrt-price for this pool
* @param whirlpoolsConfig - The public key for the WhirlpoolsConfig this pool is initialized in
* @param whirlpoolPda - PDA for the whirlpool account that would be initialized
* @param tokenMintA - Mint public key for token A
* @param tokenMintB - Mint public key for token B
* @param tokenVaultAKeypair - Keypair of the token A vault for this pool
* @param tokenVaultBKeypair - Keypair of the token B vault for this pool
* @param feeTierKey - PublicKey of the fee-tier account that this pool would use for the fee-rate
* @param tickSpacing - The desired tick spacing for this pool.
* @param funder - The account that would fund the creation of this account
*/
export type InitPoolParams = {
initSqrtPrice: BN;
whirlpoolsConfig: PublicKey;
whirlpoolPda: PDA;
tokenMintA: PublicKey;
tokenMintB: PublicKey;
tokenVaultAKeypair: Keypair;
tokenVaultBKeypair: Keypair;
feeTierKey: PublicKey;
tickSpacing: number;
funder: PublicKey;
};
/**
* Initializes a tick_array account to represent a tick-range in a Whirlpool.
*
* Special Errors
* `InvalidTokenMintOrder` - The order of mints have to be ordered by
* `SqrtPriceOutOfBounds` - provided initial_sqrt_price is not between 2^-64 to 2^64
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - InitPoolParams object
* @returns - Instruction to perform the action.
*/
export function initializePoolIx(program: Program<Whirlpool>, params: InitPoolParams): Instruction {
const {
initSqrtPrice,
tokenMintA,
tokenMintB,
whirlpoolConfigKey,
whirlpoolsConfig,
whirlpoolPda,
feeTierKey,
tokenVaultAKeypair,
@ -27,9 +67,9 @@ export function buildInitPoolIx(context: WhirlpoolContext, params: InitPoolParam
const ix = program.instruction.initializePool(whirlpoolBumps, tickSpacing, initSqrtPrice, {
accounts: {
whirlpoolsConfig: whirlpoolConfigKey,
tokenMintA: tokenMintA,
tokenMintB: tokenMintB,
whirlpoolsConfig,
tokenMintA,
tokenMintB,
funder,
whirlpool: whirlpoolPda.publicKey,
tokenVaultA: tokenVaultAKeypair.publicKey,

View File

@ -1,18 +1,52 @@
import * as anchor from "@project-serum/anchor";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { SystemProgram } from "@solana/web3.js";
import { InitializeRewardParams } from "..";
import { WhirlpoolContext } from "../context";
import { Instruction } from "../utils/transactions/transactions-builder";
import { SystemProgram, Keypair, PublicKey } from "@solana/web3.js";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
export function buildInitializeRewardIx(
context: WhirlpoolContext,
import { Instruction } from "@orca-so/common-sdk";
/**
* Parameters to initialize a rewards for a Whirlpool
*
* @category Instruction Types
* @param whirlpool - PublicKey for the whirlpool config space that the fee-tier will be initialized for.
* @param rewardIndex - The reward index that we'd like to initialize. (0 <= index <= NUM_REWARDS).
* @param rewardMint - PublicKey for the reward mint that we'd use for the reward index.
* @param rewardVaultKeypair - Keypair of the vault for this reward index.
* @param rewardAuthority - Assigned authority by the reward_super_authority for the specified reward-index in this Whirlpool
* @param funder - The account that would fund the creation of this account
*/
export type InitializeRewardParams = {
whirlpool: PublicKey;
rewardIndex: number;
rewardMint: PublicKey;
rewardVaultKeypair: Keypair;
rewardAuthority: PublicKey;
funder: PublicKey;
};
/**
* Initialize reward for a Whirlpool. A pool can only support up to a set number of rewards.
* The initial emissionsPerSecond is set to 0.
*
* #### Special Errors
* - `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized index in this pool,
* or exceeds NUM_REWARDS, or all reward slots for this pool has been initialized.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - InitializeRewardParams object
* @returns - Instruction to perform the action.
*/
export function initializeRewardIx(
program: Program<Whirlpool>,
params: InitializeRewardParams
): Instruction {
const { rewardAuthority, funder, whirlpool, rewardMint, rewardVaultKeypair, rewardIndex } =
params;
const ix = context.program.instruction.initializeReward(rewardIndex, {
const ix = program.instruction.initializeReward(rewardIndex, {
accounts: {
rewardAuthority,
funder,

View File

@ -1,14 +1,41 @@
import { WhirlpoolContext } from "../context";
import { Instruction } from "../utils/transactions/transactions-builder";
import { InitTickArrayParams } from "..";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
import { Instruction } from "@orca-so/common-sdk";
import * as anchor from "@project-serum/anchor";
import { PublicKey } from "@solana/web3.js";
import { PDA } from "@orca-so/common-sdk";
export function buildInitTickArrayIx(
context: WhirlpoolContext,
/**
* Parameters to initialize a TickArray account.
*
* @category Instruction Types
* @param whirlpool - PublicKey for the whirlpool that the initialized tick-array will host ticks for.
* @param tickArrayPda - PDA for the tick array account that will be initialized
* @param startTick - The starting tick index for this tick-array. Has to be a multiple of TickArray size & the tick spacing of this pool.
* @param funder - The account that would fund the creation of this account
*/
export type InitTickArrayParams = {
whirlpool: PublicKey;
tickArrayPda: PDA;
startTick: number;
funder: PublicKey;
};
/**
* Initializes a TickArray account.
*
* #### Special Errors
* `InvalidStartTick` - if the provided start tick is out of bounds or is not a multiple of TICK_ARRAY_SIZE * tick spacing.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - InitTickArrayParams object
* @returns - Instruction to perform the action.
*/
export function initTickArrayIx(
program: Program<Whirlpool>,
params: InitTickArrayParams
): Instruction {
const program = context.program;
const { whirlpool, funder, tickArrayPda } = params;
const ix = program.instruction.initializeTickArray(params.startTick, {

View File

@ -1,17 +1,52 @@
import { WhirlpoolContext } from "../context";
import { PublicKey, SystemProgram } from "@solana/web3.js";
import { Instruction } from "../utils/transactions/transactions-builder";
import { OpenPositionParams, PDA } from "..";
import * as anchor from "@project-serum/anchor";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
import { PublicKey } from "@solana/web3.js";
import { PDA, Instruction } from "@orca-so/common-sdk";
import { METADATA_PROGRAM_ADDRESS } from "..";
import {
OpenPositionBumpsData,
OpenPositionWithMetadataBumpsData,
} from "../types/public/anchor-types";
import { TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { METADATA_PROGRAM_ADDRESS } from "../utils/public";
import { openPositionAccounts } from "../utils/instructions-util";
export function buildOpenPositionIx(
context: WhirlpoolContext,
/**
* Parameters to open a position in a Whirlpool.
*
* @category Instruction Types
* @param whirlpool - PublicKey for the whirlpool that the position will be opened for.
* @param ownerKey - PublicKey for the wallet that will host the minted position token.
* @param positionPda - PDA for the derived position address.
* @param positionMintAddress - PublicKey for the mint token for the Position token.
* @param positionTokenAccount - The associated token address for the position token in the owners wallet.
* @param tickLowerIndex - The tick specifying the lower end of the position range.
* @param tickUpperIndex - The tick specifying the upper end of the position range.
* @param funder - The account that would fund the creation of this account
*/
export type OpenPositionParams = {
whirlpool: PublicKey;
owner: PublicKey;
positionPda: PDA;
positionMintAddress: PublicKey;
positionTokenAccount: PublicKey;
tickLowerIndex: number;
tickUpperIndex: number;
funder: PublicKey;
};
/**
* Open a position in a Whirlpool. A unique token will be minted to represent the position in the users wallet.
* The position will start off with 0 liquidity.
*
* #### Special Errors
* `InvalidTickIndex` - If a provided tick is out of bounds, out of order or not a multiple of the tick-spacing in this pool.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - OpenPositionParams object
* @returns - Instruction to perform the action.
*/
export function openPositionIx(
program: Program<Whirlpool>,
params: OpenPositionParams
): Instruction {
const { positionPda, tickLowerIndex, tickUpperIndex } = params;
@ -20,10 +55,11 @@ export function buildOpenPositionIx(
positionBump: positionPda.bump,
};
const ix = context.program.instruction.openPosition(bumps, tickLowerIndex, tickUpperIndex, {
const ix = program.instruction.openPosition(bumps, tickLowerIndex, tickUpperIndex, {
accounts: openPositionAccounts(params),
});
// TODO: Require Keypair and auto sign this ix
return {
instructions: [ix],
cleanupInstructions: [],
@ -31,8 +67,21 @@ export function buildOpenPositionIx(
};
}
export function buildOpenPositionWithMetadataIx(
context: WhirlpoolContext,
/**
* Open a position in a Whirlpool. A unique token will be minted to represent the position
* in the users wallet. Additional Metaplex metadata is appended to identify the token.
* The position will start off with 0 liquidity.
*
* #### Special Errors
* `InvalidTickIndex` - If a provided tick is out of bounds, out of order or not a multiple of the tick-spacing in this pool.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - OpenPositionParams object and a derived PDA that hosts the position's metadata.
* @returns - Instruction to perform the action.
*/
export function openPositionWithMetadataIx(
program: Program<Whirlpool>,
params: OpenPositionParams & { metadataPda: PDA }
): Instruction {
const { positionPda, metadataPda, tickLowerIndex, tickUpperIndex } = params;
@ -42,46 +91,19 @@ export function buildOpenPositionWithMetadataIx(
metadataBump: metadataPda.bump,
};
const ix = context.program.instruction.openPositionWithMetadata(
bumps,
tickLowerIndex,
tickUpperIndex,
{
accounts: {
...openPositionAccounts(params),
positionMetadataAccount: metadataPda.publicKey,
metadataProgram: METADATA_PROGRAM_ADDRESS,
metadataUpdateAuth: new PublicKey("3axbTs2z5GBy6usVbNVoqEgZMng3vZvMnAoX29BFfwhr"),
},
}
);
const ix = program.instruction.openPositionWithMetadata(bumps, tickLowerIndex, tickUpperIndex, {
accounts: {
...openPositionAccounts(params),
positionMetadataAccount: metadataPda.publicKey,
metadataProgram: METADATA_PROGRAM_ADDRESS,
metadataUpdateAuth: new PublicKey("3axbTs2z5GBy6usVbNVoqEgZMng3vZvMnAoX29BFfwhr"),
},
});
// TODO: Require Keypair and auto sign this ix
return {
instructions: [ix],
cleanupInstructions: [],
signers: [],
};
}
export function openPositionAccounts(params: OpenPositionParams) {
const {
funder,
ownerKey,
positionPda,
positionMintAddress,
positionTokenAccountAddress,
whirlpoolKey,
} = params;
return {
funder: funder,
owner: ownerKey,
position: positionPda.publicKey,
positionMint: positionMintAddress,
positionTokenAccount: positionTokenAccountAddress,
whirlpool: whirlpoolKey,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
};
}

View File

@ -1,15 +1,39 @@
import { SetCollectProtocolFeesAuthorityParams } from "..";
import { WhirlpoolContext } from "../context";
import { Instruction } from "../utils/transactions/transactions-builder";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
import { Instruction } from "@orca-so/common-sdk";
import { PublicKey } from "@solana/web3.js";
export function buildSetCollectProtocolFeesAuthorityIx(
context: WhirlpoolContext,
/**
* Parameters to set the collect fee authority in a WhirlpoolsConfig
*
* @category Instruction Types
* @param whirlpoolsConfig - The public key for the WhirlpoolsConfig this pool is initialized in
* @param collectProtocolFeesAuthority - The current collectProtocolFeesAuthority in the WhirlpoolsConfig
* @param newCollectProtocolFeesAuthority - The new collectProtocolFeesAuthority in the WhirlpoolsConfig
*/
export type SetCollectProtocolFeesAuthorityParams = {
whirlpoolsConfig: PublicKey;
collectProtocolFeesAuthority: PublicKey;
newCollectProtocolFeesAuthority: PublicKey;
};
/**
* Sets the fee authority to collect protocol fees for a WhirlpoolsConfig.
* Only the current collect protocol fee authority has permission to invoke this instruction.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - SetCollectProtocolFeesAuthorityParams object
* @returns - Instruction to perform the action.
*/
export function setCollectProtocolFeesAuthorityIx(
program: Program<Whirlpool>,
params: SetCollectProtocolFeesAuthorityParams
): Instruction {
const { whirlpoolsConfig, collectProtocolFeesAuthority, newCollectProtocolFeesAuthority } =
params;
const ix = context.program.instruction.setCollectProtocolFeesAuthority({
const ix = program.instruction.setCollectProtocolFeesAuthority({
accounts: {
whirlpoolsConfig,
collectProtocolFeesAuthority,

View File

@ -1,16 +1,47 @@
import { getFeeTierPda, SetDefaultFeeRateParams } from "..";
import { WhirlpoolContext } from "../context";
import { Instruction } from "../utils/transactions/transactions-builder";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
import { Instruction } from "@orca-so/common-sdk";
import { PublicKey } from "@solana/web3.js";
export function buildSetDefaultFeeRateIx(
context: WhirlpoolContext,
import { PDAUtil } from "../utils/public";
/**
* Parameters to set the default fee rate for a FeeTier.
*
* @category Instruction Types
* @param whirlpoolsConfig - The public key for the WhirlpoolsConfig this fee-tier is initialized in
* @param feeAuthority - Authority authorized in the WhirlpoolsConfig to set default fee rates.
* @param tickSpacing - The tick spacing of the fee-tier that we would like to update.
* @param defaultFeeRate - The new default fee rate for this fee-tier. Stored as a hundredths of a basis point.
*/
export type SetDefaultFeeRateParams = {
whirlpoolsConfig: PublicKey;
feeAuthority: PublicKey;
tickSpacing: number;
defaultFeeRate: number;
};
/**
* Updates a fee tier account with a new default fee rate. The new rate will not retroactively update
* initialized pools.
*
* #### Special Errors
* - `FeeRateMaxExceeded` - If the provided default_fee_rate exceeds MAX_FEE_RATE.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - SetDefaultFeeRateParams object
* @returns - Instruction to perform the action.
*/
export function setDefaultFeeRateIx(
program: Program<Whirlpool>,
params: SetDefaultFeeRateParams
): Instruction {
const { whirlpoolsConfig, feeAuthority, tickSpacing, defaultFeeRate } = params;
const feeTierPda = getFeeTierPda(context.program.programId, whirlpoolsConfig, tickSpacing);
const feeTierPda = PDAUtil.getFeeTier(program.programId, whirlpoolsConfig, tickSpacing);
const ix = context.program.instruction.setDefaultFeeRate(defaultFeeRate, {
const ix = program.instruction.setDefaultFeeRate(defaultFeeRate, {
accounts: {
whirlpoolsConfig,
feeTier: feeTierPda.publicKey,

View File

@ -1,14 +1,41 @@
import { SetDefaultProtocolFeeRateParams } from "..";
import { WhirlpoolContext } from "../context";
import { Instruction } from "../utils/transactions/transactions-builder";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
import { Instruction } from "@orca-so/common-sdk";
import { PublicKey } from "@solana/web3.js";
export function buildSetDefaultProtocolFeeRateIx(
context: WhirlpoolContext,
/**
* Parameters to set the default fee rate for a FeeTier.
*
* @category Instruction Types
* @param whirlpoolsConfig - The public key for the WhirlpoolsConfig this pool is initialized in
* @param feeAuthority - Authority authorized in the WhirlpoolsConfig to set default fee rates.
* @param defaultProtocolFeeRate - The new default protocol fee rate for this config. Stored as a basis point of the total fees collected by feeRate.
*/
export type SetDefaultProtocolFeeRateParams = {
whirlpoolsConfig: PublicKey;
feeAuthority: PublicKey;
defaultProtocolFeeRate: number;
};
/**
* Updates a WhirlpoolsConfig with a new default protocol fee rate. The new rate will not retroactively update
* initialized pools.
*
* #### Special Errors
* - `ProtocolFeeRateMaxExceeded` - If the provided default_protocol_fee_rate exceeds MAX_PROTOCOL_FEE_RATE.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - SetDefaultFeeRateParams object
* @returns - Instruction to perform the action.
*/
export function setDefaultProtocolFeeRateIx(
program: Program<Whirlpool>,
params: SetDefaultProtocolFeeRateParams
): Instruction {
const { whirlpoolsConfig, feeAuthority, defaultProtocolFeeRate } = params;
const ix = context.program.instruction.setDefaultProtocolFeeRate(defaultProtocolFeeRate, {
const ix = program.instruction.setDefaultProtocolFeeRate(defaultProtocolFeeRate, {
accounts: {
whirlpoolsConfig,
feeAuthority,

View File

@ -1,14 +1,39 @@
import { SetFeeAuthorityParams } from "..";
import { WhirlpoolContext } from "../context";
import { Instruction } from "../utils/transactions/transactions-builder";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
import { Instruction } from "@orca-so/common-sdk";
import { PublicKey } from "@solana/web3.js";
export function buildSetFeeAuthorityIx(
context: WhirlpoolContext,
/**
* Parameters to set the fee authority in a WhirlpoolsConfig
*
* @category Instruction Types
* @param whirlpoolsConfig - The public key for the WhirlpoolsConfig this pool is initialized in
* @param feeAuthority - The current feeAuthority in the WhirlpoolsConfig
* @param newFeeAuthority - The new feeAuthority in the WhirlpoolsConfig
*/
export type SetFeeAuthorityParams = {
whirlpoolsConfig: PublicKey;
feeAuthority: PublicKey;
newFeeAuthority: PublicKey;
};
/**
* Sets the fee authority for a WhirlpoolsConfig.
* The fee authority can set the fee & protocol fee rate for individual pools or set the default fee rate for newly minted pools.
* Only the current fee authority has permission to invoke this instruction.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - SetFeeAuthorityParams object
* @returns - Instruction to perform the action.
*/
export function setFeeAuthorityIx(
program: Program<Whirlpool>,
params: SetFeeAuthorityParams
): Instruction {
const { whirlpoolsConfig, feeAuthority, newFeeAuthority } = params;
const ix = context.program.instruction.setFeeAuthority({
const ix = program.instruction.setFeeAuthority({
accounts: {
whirlpoolsConfig,
feeAuthority,

View File

@ -1,14 +1,40 @@
import { SetFeeRateParams } from "..";
import { WhirlpoolContext } from "../context";
import { Instruction } from "../utils/transactions/transactions-builder";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
import { Instruction } from "@orca-so/common-sdk";
import { PublicKey } from "@solana/web3.js";
export function buildSetFeeRateIx(
context: WhirlpoolContext,
params: SetFeeRateParams
): Instruction {
/**
* Parameters to set fee rate for a Whirlpool.
*
* @category Instruction Types
* @param whirlpool - PublicKey for the whirlpool to update. This whirlpool has to be part of the provided WhirlpoolsConfig space.
* @param whirlpoolsConfig - The public key for the WhirlpoolsConfig this pool is initialized in
* @param feeAuthority - Authority authorized in the WhirlpoolsConfig to set default fee rates.
* @param feeRate - The new fee rate for this fee-tier. Stored as a hundredths of a basis point.
*/
export type SetFeeRateParams = {
whirlpool: PublicKey;
whirlpoolsConfig: PublicKey;
feeAuthority: PublicKey;
feeRate: number;
};
/**
* Sets the fee rate for a Whirlpool.
* Only the current fee authority has permission to invoke this instruction.
*
* #### Special Errors
* - `FeeRateMaxExceeded` - If the provided fee_rate exceeds MAX_FEE_RATE.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - SetFeeRateParams object
* @returns - Instruction to perform the action.
*/
export function setFeeRateIx(program: Program<Whirlpool>, params: SetFeeRateParams): Instruction {
const { whirlpoolsConfig, whirlpool, feeAuthority, feeRate } = params;
const ix = context.program.instruction.setFeeRate(feeRate, {
const ix = program.instruction.setFeeRate(feeRate, {
accounts: {
whirlpoolsConfig,
whirlpool,

View File

@ -1,14 +1,43 @@
import { SetProtocolFeeRateParams } from "..";
import { WhirlpoolContext } from "../context";
import { Instruction } from "../utils/transactions/transactions-builder";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
import { Instruction } from "@orca-so/common-sdk";
import { PublicKey } from "@solana/web3.js";
export function buildSetProtocolFeeRateIx(
context: WhirlpoolContext,
/**
* Parameters to set fee rate for a Whirlpool.
*
* @category Instruction Types
* @param whirlpool - PublicKey for the whirlpool to update. This whirlpool has to be part of the provided WhirlpoolsConfig space.
* @param whirlpoolsConfig - The public key for the WhirlpoolsConfig this pool is initialized in
* @param feeAuthority - Authority authorized in the WhirlpoolsConfig to set default fee rates.
* @param protocolFeeRate - The new default protocol fee rate for this pool. Stored as a basis point of the total fees collected by feeRate.
*/
export type SetProtocolFeeRateParams = {
whirlpool: PublicKey;
whirlpoolsConfig: PublicKey;
feeAuthority: PublicKey;
protocolFeeRate: number;
};
/**
* Sets the protocol fee rate for a Whirlpool.
* Only the current fee authority has permission to invoke this instruction.
*
* #### Special Errors
* - `ProtocolFeeRateMaxExceeded` - If the provided default_protocol_fee_rate exceeds MAX_PROTOCOL_FEE_RATE.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - SetFeeRateParams object
* @returns - Instruction to perform the action.
*/
export function setProtocolFeeRateIx(
program: Program<Whirlpool>,
params: SetProtocolFeeRateParams
): Instruction {
const { whirlpoolsConfig, whirlpool, feeAuthority, protocolFeeRate } = params;
const ix = context.program.instruction.setProtocolFeeRate(protocolFeeRate, {
const ix = program.instruction.setProtocolFeeRate(protocolFeeRate, {
accounts: {
whirlpoolsConfig,
whirlpool,

View File

@ -1,9 +1,41 @@
import { WhirlpoolContext } from "../context";
import { Instruction } from "../utils/transactions/transactions-builder";
import { SetRewardAuthorityBySuperAuthorityParams } from "..";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
import { Instruction } from "@orca-so/common-sdk";
import { PublicKey } from "@solana/web3.js";
export function buildSetRewardAuthorityBySuperAuthorityIx(
context: WhirlpoolContext,
/**
* Parameters to update the reward authority at a particular rewardIndex on a Whirlpool.
*
* @category Instruction Types
* @param whirlpool - PublicKey for the whirlpool to update. This whirlpool has to be part of the provided WhirlpoolsConfig space.
* @param whirlpoolsConfig - The public key for the WhirlpoolsConfig this pool is initialized in
* @param rewardIndex - The reward index that we'd like to update. (0 <= index <= NUM_REWARDS).
* @param rewardEmissionsSuperAuthority - The current rewardEmissionsSuperAuthority in the WhirlpoolsConfig
* @param newRewardAuthority - The new rewardAuthority in the Whirlpool at the rewardIndex
*/
export type SetRewardAuthorityBySuperAuthorityParams = {
whirlpool: PublicKey;
whirlpoolsConfig: PublicKey;
rewardIndex: number;
rewardEmissionsSuperAuthority: PublicKey;
newRewardAuthority: PublicKey;
};
/**
* Set the whirlpool reward authority at the provided `reward_index`.
* Only the current reward super authority has permission to invoke this instruction.
*
* #### Special Errors
* - `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized index in this pool,
* or exceeds NUM_REWARDS.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - SetRewardAuthorityParams object
* @returns - Instruction to perform the action.
*/
export function setRewardAuthorityBySuperAuthorityIx(
program: Program<Whirlpool>,
params: SetRewardAuthorityBySuperAuthorityParams
): Instruction {
const {
@ -14,7 +46,7 @@ export function buildSetRewardAuthorityBySuperAuthorityIx(
rewardIndex,
} = params;
const ix = context.program.instruction.setRewardAuthorityBySuperAuthority(rewardIndex, {
const ix = program.instruction.setRewardAuthorityBySuperAuthority(rewardIndex, {
accounts: {
whirlpoolsConfig,
whirlpool,

View File

@ -1,13 +1,43 @@
import { WhirlpoolContext } from "../context";
import { Instruction } from "../utils/transactions/transactions-builder";
import { SetRewardAuthorityParams } from "..";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
import { Instruction } from "@orca-so/common-sdk";
import { PublicKey } from "@solana/web3.js";
export function buildSetRewardAuthorityIx(
context: WhirlpoolContext,
/**
* Parameters to update the reward authority at a particular rewardIndex on a Whirlpool.
*
* @category Instruction Types
* @param whirlpool - PublicKey for the whirlpool to update.
* @param rewardIndex - The reward index that we'd like to update. (0 <= index <= NUM_REWARDS).
* @param rewardAuthority - The current rewardAuthority in the Whirlpool at the rewardIndex
* @param newRewardAuthority - The new rewardAuthority in the Whirlpool at the rewardIndex
*/
export type SetRewardAuthorityParams = {
whirlpool: PublicKey;
rewardIndex: number;
rewardAuthority: PublicKey;
newRewardAuthority: PublicKey;
};
/**
* Set the whirlpool reward authority at the provided `reward_index`.
* Only the current reward authority for this reward index has permission to invoke this instruction.
*
* #### Special Errors
* - `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized index in this pool,
* or exceeds NUM_REWARDS.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - SetRewardAuthorityParams object
* @returns - Instruction to perform the action.
*/
export function setRewardAuthorityIx(
program: Program<Whirlpool>,
params: SetRewardAuthorityParams
): Instruction {
const { whirlpool, rewardAuthority, newRewardAuthority, rewardIndex } = params;
const ix = context.program.instruction.setRewardAuthority(rewardIndex, {
const ix = program.instruction.setRewardAuthority(rewardIndex, {
accounts: {
whirlpool,
rewardAuthority,

View File

@ -1,14 +1,54 @@
import { SetRewardEmissionsParams } from "..";
import { WhirlpoolContext } from "../context";
import { Instruction } from "../utils/transactions/transactions-builder";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
import { Instruction } from "@orca-so/common-sdk";
import { PublicKey } from "@solana/web3.js";
import { BN } from "@project-serum/anchor";
export function buildSetRewardEmissionsIx(
context: WhirlpoolContext,
/**
* Parameters to set rewards emissions for a reward in a Whirlpool
*
* @category Instruction Types
* @param whirlpool - PublicKey for the whirlpool which the reward resides in.
* @param rewardIndex - The reward index that we'd like to initialize. (0 <= index <= NUM_REWARDS).
* @param rewardVaultKey - PublicKey of the vault for this reward index.
* @param rewardAuthority - Assigned authority by the reward_super_authority for the specified reward-index in this Whirlpool
* @param emissionsPerSecondX64 - The new emissions per second to set for this reward.
*/
export type SetRewardEmissionsParams = {
whirlpool: PublicKey;
rewardIndex: number;
rewardVaultKey: PublicKey;
rewardAuthority: PublicKey;
emissionsPerSecondX64: BN;
};
/**
* Set the reward emissions for a reward in a Whirlpool.
*
* #### Special Errors
* - `RewardVaultAmountInsufficient` - The amount of rewards in the reward vault cannot emit more than a day of desired emissions.
* - `InvalidTimestamp` - Provided timestamp is not in order with the previous timestamp.
* - `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized index in this pool,
* or exceeds NUM_REWARDS.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - SetRewardEmissionsParams object
* @returns - Instruction to perform the action.
*/
export function setRewardEmissionsIx(
program: Program<Whirlpool>,
params: SetRewardEmissionsParams
): Instruction {
const { rewardAuthority, whirlpool, rewardIndex, rewardVault, emissionsPerSecondX64 } = params;
const {
rewardAuthority,
whirlpool,
rewardIndex,
rewardVaultKey: rewardVault,
emissionsPerSecondX64,
} = params;
const ix = context.program.instruction.setRewardEmissions(rewardIndex, emissionsPerSecondX64, {
const ix = program.instruction.setRewardEmissions(rewardIndex, emissionsPerSecondX64, {
accounts: {
rewardAuthority,
whirlpool,

View File

@ -1,15 +1,40 @@
import { WhirlpoolContext } from "../context";
import { Instruction } from "../utils/transactions/transactions-builder";
import { SetRewardEmissionsSuperAuthorityParams } from "..";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
import { Instruction } from "@orca-so/common-sdk";
import { PublicKey } from "@solana/web3.js";
export function buildSetRewardEmissionsSuperAuthorityIx(
context: WhirlpoolContext,
/**
* Parameters to set rewards emissions for a reward in a Whirlpool
*
* @category Instruction Types
* @param whirlpoolsConfig - PublicKey for the WhirlpoolsConfig that we want to update.
* @param rewardEmissionsSuperAuthority - Current reward emission super authority in this WhirlpoolsConfig
* @param newRewardEmissionsSuperAuthority - New reward emission super authority for this WhirlpoolsConfig
*/
export type SetRewardEmissionsSuperAuthorityParams = {
whirlpoolsConfig: PublicKey;
rewardEmissionsSuperAuthority: PublicKey;
newRewardEmissionsSuperAuthority: PublicKey;
};
/**
* Set the whirlpool reward super authority for a WhirlpoolsConfig
* Only the current reward super authority has permission to invoke this instruction.
* This instruction will not change the authority on any `WhirlpoolRewardInfo` whirlpool rewards.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - SetRewardEmissionsSuperAuthorityParams object
* @returns - Instruction to perform the action.
*/
export function setRewardEmissionsSuperAuthorityIx(
program: Program<Whirlpool>,
params: SetRewardEmissionsSuperAuthorityParams
): Instruction {
const { whirlpoolsConfig, rewardEmissionsSuperAuthority, newRewardEmissionsSuperAuthority } =
params;
const ix = context.program.instruction.setRewardEmissionsSuperAuthority({
const ix = program.instruction.setRewardEmissionsSuperAuthority({
accounts: {
whirlpoolsConfig,
rewardEmissionsSuperAuthority: rewardEmissionsSuperAuthority,

View File

@ -1,9 +1,86 @@
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { SwapParams } from "..";
import { WhirlpoolContext } from "../context";
import { Instruction } from "../utils/transactions/transactions-builder";
import { TOKEN_PROGRAM_ID, u64 } from "@solana/spl-token";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
import { Instruction } from "@orca-so/common-sdk";
import { PublicKey } from "@solana/web3.js";
import { BN } from "@project-serum/anchor";
export function buildSwapIx(context: WhirlpoolContext, params: SwapParams): Instruction {
/**
* Parameters and accounts to swap on a Whirlpool
*
* @category Instruction Types
* @param aToB - The direction of the swap. True if swapping from A to B. False if swapping from B to A.
* @param amountSpecifiedIsInput - Specifies the token the parameter `amount`represents. If true, the amount represents
* the input token of the swap.
* @param amount - The amount of input or output token to swap from (depending on amountSpecifiedIsInput).
* @param otherAmountThreshold - The maximum/minimum of input/output token to swap into (depending on amountSpecifiedIsInput).
* @param sqrtPriceLimit - The maximum/minimum price the swap will swap to.
* @param tickArray0 - PublicKey of the tick-array where the Whirlpool's currentTickIndex resides in
* @param tickArray1 - The next tick-array in the swap direction. If the swap will not reach the next tick-aray, input the same array as tickArray0.
* @param tickArray2 - The next tick-array in the swap direction after tickArray2. If the swap will not reach the next tick-aray, input the same array as tickArray1.
* @param whirlpool - PublicKey for the whirlpool that the position will be opened for.
* @param tokenOwnerAccountA - PublicKey for the associated token account for tokenA in the collection wallet
* @param tokenOwnerAccountB - PublicKey for the associated token account for tokenB in the collection wallet
* @param tokenVaultA - PublicKey for the tokenA vault for this whirlpool.
* @param tokenVaultB - PublicKey for the tokenB vault for this whirlpool.
* @param oracle - PublicKey for the oracle account for this Whirlpool.
* @param tokenAuthority - authority to withdraw tokens from the input token account
*/
export type SwapParams = SwapInput & {
whirlpool: PublicKey;
tokenOwnerAccountA: PublicKey;
tokenOwnerAccountB: PublicKey;
tokenVaultA: PublicKey;
tokenVaultB: PublicKey;
oracle: PublicKey;
tokenAuthority: PublicKey;
};
/**
* Parameters to swap on a Whirlpool
*
* @category Instruction Types
* @param aToB - The direction of the swap. True if swapping from A to B. False if swapping from B to A.
* @param amountSpecifiedIsInput - Specifies the token the parameter `amount`represents. If true, the amount represents
* the input token of the swap.
* @param amount - The amount of input or output token to swap from (depending on amountSpecifiedIsInput).
* @param otherAmountThreshold - The maximum/minimum of input/output token to swap into (depending on amountSpecifiedIsInput).
* @param sqrtPriceLimit - The maximum/minimum price the swap will swap to.
* @param tickArray0 - PublicKey of the tick-array where the Whirlpool's currentTickIndex resides in
* @param tickArray1 - The next tick-array in the swap direction. If the swap will not reach the next tick-aray, input the same array as tickArray0.
* @param tickArray2 - The next tick-array in the swap direction after tickArray2. If the swap will not reach the next tick-aray, input the same array as tickArray1.
*/
export type SwapInput = {
amount: u64;
otherAmountThreshold: u64;
sqrtPriceLimit: BN;
amountSpecifiedIsInput: boolean;
aToB: boolean;
tickArray0: PublicKey;
tickArray1: PublicKey;
tickArray2: PublicKey;
};
/**
* Perform a swap in this Whirlpool
*
* #### Special Errors
* - `ZeroTradableAmount` - User provided parameter `amount` is 0.
* - `InvalidSqrtPriceLimitDirection` - User provided parameter `sqrt_price_limit` does not match the direction of the trade.
* - `SqrtPriceOutOfBounds` - User provided parameter `sqrt_price_limit` is over Whirlppool's max/min bounds for sqrt-price.
* - `InvalidTickArraySequence` - User provided tick-arrays are not in sequential order required to proceed in this trade direction.
* - `TickArraySequenceInvalidIndex` - The swap loop attempted to access an invalid array index during the query of the next initialized tick.
* - `TickArrayIndexOutofBounds` - The swap loop attempted to access an invalid array index during tick crossing.
* - `LiquidityOverflow` - Liquidity value overflowed 128bits during tick crossing.
* - `InvalidTickSpacing` - The swap pool was initialized with tick-spacing of 0.
*
* ### Parameters
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - SwapParams object
* @returns - Instruction to perform the action.
*/
export function swapIx(program: Program<Whirlpool>, params: SwapParams): Instruction {
const {
amount,
otherAmountThreshold,
@ -22,7 +99,7 @@ export function buildSwapIx(context: WhirlpoolContext, params: SwapParams): Inst
oracle,
} = params;
const ix = context.program.instruction.swap(
const ix = program.instruction.swap(
amount,
otherAmountThreshold,
sqrtPriceLimit,

View File

@ -1,14 +1,44 @@
import { UpdateFeesAndRewardsParams } from "..";
import { WhirlpoolContext } from "../context";
import { Instruction } from "../utils/transactions/transactions-builder";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
import { PublicKey } from "@solana/web3.js";
export function buildUpdateFeesAndRewardsIx(
context: WhirlpoolContext,
import { Instruction } from "@orca-so/common-sdk";
/**
* Parameters to update fees and reward values for a position.
*
* @category Instruction Types
* @param whirlpool - PublicKey for the whirlpool that the position will be opened for.
* @param position - PublicKey for the position will be opened for.
* @param tickArrayLower - PublicKey for the tick-array account that hosts the tick at the lower tick index.
* @param tickArrayUpper - PublicKey for the tick-array account that hosts the tick at the upper tick index.
*/
export type UpdateFeesAndRewardsParams = {
whirlpool: PublicKey;
position: PublicKey;
tickArrayLower: PublicKey;
tickArrayUpper: PublicKey;
};
/**
* Update the accrued fees and rewards for a position.
*
* #### Special Errors
* `TickNotFound` - Provided tick array account does not contain the tick for this position.
* `LiquidityZero` - Position has zero liquidity and therefore already has the most updated fees and reward values.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - UpdateFeesAndRewardsParams object
* @returns - Instruction to perform the action.
*/
export function updateFeesAndRewardsIx(
program: Program<Whirlpool>,
params: UpdateFeesAndRewardsParams
): Instruction {
const { whirlpool, position, tickArrayLower, tickArrayUpper } = params;
const ix = context.program.instruction.updateFeesAndRewards({
const ix = program.instruction.updateFeesAndRewards({
accounts: {
whirlpool,
position,

418
sdk/src/ix.ts Normal file
View File

@ -0,0 +1,418 @@
import { PDA } from "@orca-so/common-sdk";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "./artifacts/whirlpool";
import * as ix from "./instructions";
/**
* Instruction set for the Whirlpools program.
*
* @category Core
*/
export class WhirlpoolIx {
/**
* Initializes a WhirlpoolsConfig account that hosts info & authorities
* required to govern a set of Whirlpools.
*
* @param program - program object containing services required to generate the instruction
* @param params - InitConfigParams object
* @returns - Instruction to perform the action.
*/
public static initializeConfigIx(program: Program<Whirlpool>, params: ix.InitConfigParams) {
return ix.initializeConfigIx(program, params);
}
/**
* Initializes a fee tier account usable by Whirlpools in this WhirlpoolsConfig space.
*
* Special Errors
* `FeeRateMaxExceeded` - If the provided default_fee_rate exceeds MAX_FEE_RATE.
*
* @param program - program object containing services required to generate the instruction
* @param params - InitFeeTierParams object
* @returns - Instruction to perform the action.
*/
public static initializeFeeTierIx(program: Program<Whirlpool>, params: ix.InitFeeTierParams) {
return ix.initializeFeeTierIx(program, params);
}
/**
* Initializes a tick_array account to represent a tick-range in a Whirlpool.
*
* Special Errors
* `InvalidTokenMintOrder` - The order of mints have to be ordered by
* `SqrtPriceOutOfBounds` - provided initial_sqrt_price is not between 2^-64 to 2^64
*
* @param program - program object containing services required to generate the instruction
* @param params - InitPoolParams object
* @returns - Instruction to perform the action.
*/
public static initializePoolIx(program: Program<Whirlpool>, params: ix.InitPoolParams) {
return ix.initializePoolIx(program, params);
}
/**
* Initialize reward for a Whirlpool. A pool can only support up to a set number of rewards.
* The initial emissionsPerSecond is set to 0.
*
* #### Special Errors
* - `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized index in this pool,
* or exceeds NUM_REWARDS, or all reward slots for this pool has been initialized.
*
* @param program - program object containing services required to generate the instruction
* @param params - InitializeRewardParams object
* @returns - Instruction to perform the action.
*/
public static initializeRewardIx(program: Program<Whirlpool>, params: ix.InitializeRewardParams) {
return ix.initializeRewardIx(program, params);
}
/**
* Initializes a TickArray account.
*
* #### Special Errors
* `InvalidStartTick` - if the provided start tick is out of bounds or is not a multiple of TICK_ARRAY_SIZE * tick spacing.
*
* @param program - program object containing services required to generate the instruction
* @param params - InitTickArrayParams object
* @returns - Instruction to perform the action.
*/
public static initTickArrayIx(program: Program<Whirlpool>, params: ix.InitTickArrayParams) {
return ix.initTickArrayIx(program, params);
}
/**
* Open a position in a Whirlpool. A unique token will be minted to represent the position in the users wallet.
* The position will start off with 0 liquidity.
*
* #### Special Errors
* `InvalidTickIndex` - If a provided tick is out of bounds, out of order or not a multiple of the tick-spacing in this pool.
*
* @param program - program object containing services required to generate the instruction
* @param params - OpenPositionParams object
* @returns - Instruction to perform the action.
*/
public static openPositionIx(program: Program<Whirlpool>, params: ix.OpenPositionParams) {
return ix.openPositionIx(program, params);
}
/**
* Open a position in a Whirlpool. A unique token will be minted to represent the position
* in the users wallet. Additional Metaplex metadata is appended to identify the token.
* The position will start off with 0 liquidity.
*
* #### Special Errors
* `InvalidTickIndex` - If a provided tick is out of bounds, out of order or not a multiple of the tick-spacing in this pool.
*
* @param program - program object containing services required to generate the instruction
* @param params - OpenPositionParams object and a derived PDA that hosts the position's metadata.
* @returns - Instruction to perform the action.
*/
public static openPositionWithMetadataIx(
program: Program<Whirlpool>,
params: ix.OpenPositionParams & { metadataPda: PDA }
) {
return ix.openPositionWithMetadataIx(program, params);
}
/**
* Add liquidity to a position in the Whirlpool. This call also updates the position's accrued fees and rewards.
*
* #### Special Errors
* `LiquidityZero` - Provided liquidity amount is zero.
* `LiquidityTooHigh` - Provided liquidity exceeds u128::max.
* `TokenMaxExceeded` - The required token to perform this operation exceeds the user defined amount.
*
* @param program - program object containing services required to generate the instruction
* @param params - IncreaseLiquidityParams object
* @returns - Instruction to perform the action.
*/
public static increaseLiquidityIx(
program: Program<Whirlpool>,
params: ix.IncreaseLiquidityParams
) {
return ix.increaseLiquidityIx(program, params);
}
/**
* Remove liquidity to a position in the Whirlpool. This call also updates the position's accrued fees and rewards.
*
* #### Special Errors
* - `LiquidityZero` - Provided liquidity amount is zero.
* - `LiquidityTooHigh` - Provided liquidity exceeds u128::max.
* - `TokenMinSubceeded` - The required token to perform this operation subceeds the user defined amount.
*
* @param program - program object containing services required to generate the instruction
* @param params - DecreaseLiquidityParams object
* @returns - Instruction to perform the action.
*/
public static decreaseLiquidityIx(
program: Program<Whirlpool>,
params: ix.DecreaseLiquidityParams
) {
return ix.decreaseLiquidityIx(program, params);
}
/**
* Close a position in a Whirlpool. Burns the position token in the owner's wallet.
*
* @param program - program object containing services required to generate the instruction
* @param params - ClosePositionParams object
* @returns - Instruction to perform the action.
*/
public static closePositionIx(program: Program<Whirlpool>, params: ix.ClosePositionParams) {
return ix.closePositionIx(program, params);
}
/**
* Perform a swap in this Whirlpool
*
* #### Special Errors
* - `ZeroTradableAmount` - User provided parameter `amount` is 0.
* - `InvalidSqrtPriceLimitDirection` - User provided parameter `sqrt_price_limit` does not match the direction of the trade.
* - `SqrtPriceOutOfBounds` - User provided parameter `sqrt_price_limit` is over Whirlppool's max/min bounds for sqrt-price.
* - `InvalidTickArraySequence` - User provided tick-arrays are not in sequential order required to proceed in this trade direction.
* - `TickArraySequenceInvalidIndex` - The swap loop attempted to access an invalid array index during the query of the next initialized tick.
* - `TickArrayIndexOutofBounds` - The swap loop attempted to access an invalid array index during tick crossing.
* - `LiquidityOverflow` - Liquidity value overflowed 128bits during tick crossing.
* - `InvalidTickSpacing` - The swap pool was initialized with tick-spacing of 0.
*
* ### Parameters
* @param program - program object containing services required to generate the instruction
* @param params - SwapParams object
* @returns - Instruction to perform the action.
*/
public static swapIx(program: Program<Whirlpool>, params: ix.SwapParams) {
return ix.swapIx(program, params);
}
/**
* Update the accrued fees and rewards for a position.
*
* #### Special Errors
* `TickNotFound` - Provided tick array account does not contain the tick for this position.
* `LiquidityZero` - Position has zero liquidity and therefore already has the most updated fees and reward values.
*
* @param program - program object containing services required to generate the instruction
* @param params - UpdateFeesAndRewardsParams object
* @returns - Instruction to perform the action.
*/
public static updateFeesAndRewardsIx(
program: Program<Whirlpool>,
params: ix.UpdateFeesAndRewardsParams
) {
return ix.updateFeesAndRewardsIx(program, params);
}
/**
* Collect fees accrued for this position.
* Call updateFeesAndRewards before this to update the position to the newest accrued values.
*
* @param program - program object containing services required to generate the instruction
* @param params - CollectFeesParams object
* @returns - Instruction to perform the action.
*/
public static collectFeesIx(program: Program<Whirlpool>, params: ix.CollectFeesParams) {
return ix.collectFeesIx(program, params);
}
/**
* Collect protocol fees accrued in this Whirlpool.
*
* @param program - program object containing services required to generate the instruction
* @param params - CollectProtocolFeesParams object
* @returns - Instruction to perform the action.
*/
public static collectProtocolFeesIx(
program: Program<Whirlpool>,
params: ix.CollectProtocolFeesParams
) {
return ix.collectProtocolFeesIx(program, params);
}
/**
* Collect rewards accrued for this reward index in a position.
* Call updateFeesAndRewards before this to update the position to the newest accrued values.
*
* @param program - program object containing services required to generate the instruction
* @param params - CollectRewardParams object
* @returns - Instruction to perform the action.
*/
public static collectRewardIx(program: Program<Whirlpool>, params: ix.CollectRewardParams) {
return ix.collectRewardIx(program, params);
}
/**
* Sets the fee authority to collect protocol fees for a WhirlpoolsConfig.
* Only the current collect protocol fee authority has permission to invoke this instruction.
*
* @param program - program object containing services required to generate the instruction
* @param params - SetCollectProtocolFeesAuthorityParams object
* @returns - Instruction to perform the action.
*/
public static setCollectProtocolFeesAuthorityIx(
program: Program<Whirlpool>,
params: ix.SetCollectProtocolFeesAuthorityParams
) {
return ix.setCollectProtocolFeesAuthorityIx(program, params);
}
/**
* Updates a fee tier account with a new default fee rate. The new rate will not retroactively update
* initialized pools.
*
* #### Special Errors
* - `FeeRateMaxExceeded` - If the provided default_fee_rate exceeds MAX_FEE_RATE.
*
* @param program - program object containing services required to generate the instruction
* @param params - SetDefaultFeeRateParams object
* @returns - Instruction to perform the action.
*/
public static setDefaultFeeRateIx(
program: Program<Whirlpool>,
params: ix.SetDefaultFeeRateParams
) {
return ix.setDefaultFeeRateIx(program, params);
}
/**
* Updates a WhirlpoolsConfig with a new default protocol fee rate. The new rate will not retroactively update
* initialized pools.
*
* #### Special Errors
* - `ProtocolFeeRateMaxExceeded` - If the provided default_protocol_fee_rate exceeds MAX_PROTOCOL_FEE_RATE.
*
* @param program - program object containing services required to generate the instruction
* @param params - SetDefaultFeeRateParams object
* @returns - Instruction to perform the action.
*/
public static setDefaultProtocolFeeRateIx(
program: Program<Whirlpool>,
params: ix.SetDefaultProtocolFeeRateParams
) {
return ix.setDefaultProtocolFeeRateIx(program, params);
}
/**
* Sets the fee authority for a WhirlpoolsConfig.
* The fee authority can set the fee & protocol fee rate for individual pools or set the default fee rate for newly minted pools.
* Only the current fee authority has permission to invoke this instruction.
*
* @param program - program object containing services required to generate the instruction
* @param params - SetFeeAuthorityParams object
* @returns - Instruction to perform the action.
*/
public static setFeeAuthorityIx(program: Program<Whirlpool>, params: ix.SetFeeAuthorityParams) {
return ix.setFeeAuthorityIx(program, params);
}
/**
* Sets the fee rate for a Whirlpool.
* Only the current fee authority has permission to invoke this instruction.
*
* #### Special Errors
* - `FeeRateMaxExceeded` - If the provided fee_rate exceeds MAX_FEE_RATE.
*
* @param program - program object containing services required to generate the instruction
* @param params - SetFeeRateParams object
* @returns - Instruction to perform the action.
*/
public static setFeeRateIx(program: Program<Whirlpool>, params: ix.SetFeeRateParams) {
return ix.setFeeRateIx(program, params);
}
/**
* Sets the protocol fee rate for a Whirlpool.
* Only the current fee authority has permission to invoke this instruction.
*
* #### Special Errors
* - `ProtocolFeeRateMaxExceeded` - If the provided default_protocol_fee_rate exceeds MAX_PROTOCOL_FEE_RATE.
*
* @param program - program object containing services required to generate the instruction
* @param params - SetFeeRateParams object
* @returns - Instruction to perform the action.
*/
public static setProtocolFeeRateIx(
program: Program<Whirlpool>,
params: ix.SetProtocolFeeRateParams
) {
return ix.setProtocolFeeRateIx(program, params);
}
/**
* Set the whirlpool reward authority at the provided `reward_index`.
* Only the current reward super authority has permission to invoke this instruction.
*
* #### Special Errors
* - `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized index in this pool,
* or exceeds NUM_REWARDS.
*
* @param program - program object containing services required to generate the instruction
* @param params - SetRewardAuthorityParams object
* @returns - Instruction to perform the action.
*/
public static setRewardAuthorityBySuperAuthorityIx(
program: Program<Whirlpool>,
params: ix.SetRewardAuthorityBySuperAuthorityParams
) {
return ix.setRewardAuthorityBySuperAuthorityIx(program, params);
}
/**
* Set the whirlpool reward authority at the provided `reward_index`.
* Only the current reward authority for this reward index has permission to invoke this instruction.
*
* #### Special Errors
* - `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized index in this pool,
* or exceeds NUM_REWARDS.
*
* @param program - program object containing services required to generate the instruction
* @param params - SetRewardAuthorityParams object
* @returns - Instruction to perform the action.
*/
public static setRewardAuthorityIx(
program: Program<Whirlpool>,
params: ix.SetRewardAuthorityParams
) {
return ix.setRewardAuthorityIx(program, params);
}
/**
* Set the reward emissions for a reward in a Whirlpool.
*
* #### Special Errors
* - `RewardVaultAmountInsufficient` - The amount of rewards in the reward vault cannot emit more than a day of desired emissions.
* - `InvalidTimestamp` - Provided timestamp is not in order with the previous timestamp.
* - `InvalidRewardIndex` - If the provided reward index doesn't match the lowest uninitialized index in this pool,
* or exceeds NUM_REWARDS.
*
* @param program - program object containing services required to generate the instruction
* @param params - SetRewardEmissionsParams object
* @returns - Instruction to perform the action.
*/
public static setRewardEmissionsIx(
program: Program<Whirlpool>,
params: ix.SetRewardEmissionsParams
) {
return ix.setRewardEmissionsIx(program, params);
}
/**
* Set the whirlpool reward super authority for a WhirlpoolsConfig
* Only the current reward super authority has permission to invoke this instruction.
* This instruction will not change the authority on any `WhirlpoolRewardInfo` whirlpool rewards.
*
* @param program - program object containing services required to generate the instruction
* @param params - SetRewardEmissionsSuperAuthorityParams object
* @returns - Instruction to perform the action.
*/
public static setRewardEmissionsSuperAuthorityIx(
program: Program<Whirlpool>,
params: ix.SetRewardEmissionsSuperAuthorityParams
) {
return ix.setRewardEmissionsSuperAuthorityIx(program, params);
}
}

View File

@ -0,0 +1,343 @@
import { Connection, PublicKey } from "@solana/web3.js";
import invariant from "tiny-invariant";
import { AccountInfo, AccountLayout, MintInfo } from "@solana/spl-token";
import {
ParsableEntity,
ParsableFeeTier,
ParsableMintInfo,
ParsablePosition,
ParsableTickArray,
ParsableTokenInfo,
ParsableWhirlpool,
ParsableWhirlpoolsConfig,
} from "./parsing";
import { Address } from "@project-serum/anchor";
import { PositionData, TickArrayData, WhirlpoolsConfigData, WhirlpoolData } from "../..";
import { FeeTierData } from "../../types/public";
import { AddressUtil } from "@orca-so/common-sdk";
/**
* Supported accounts
*/
type CachedValue =
| WhirlpoolsConfigData
| WhirlpoolData
| PositionData
| TickArrayData
| FeeTierData
| AccountInfo
| MintInfo;
/**
* Include both the entity (i.e. type) of the stored value, and the value itself
*/
interface CachedContent<T extends CachedValue> {
entity: ParsableEntity<T>;
value: CachedValue | null;
}
/**
* Type for rpc batch request response
*/
type GetMultipleAccountsResponse = {
error?: string;
result?: {
value?: ({ data: [string, string] } | null)[];
};
};
/**
* Data access layer to access Whirlpool related accounts
* Includes internal cache that can be refreshed by the client.
*
* @category Core
*/
export class AccountFetcher {
private readonly connection: Connection;
private readonly _cache: Record<string, CachedContent<CachedValue>> = {};
private _accountRentExempt: number | undefined;
constructor(connection: Connection, cache?: Record<string, CachedContent<CachedValue>>) {
this.connection = connection;
this._cache = cache ?? {};
}
/*** Public Methods ***/
/**
* Retrieve minimum balance for rent exemption of a Token Account;
*
* @param refresh force refresh of account rent exemption
* @returns minimum balance for rent exemption
*/
public async getAccountRentExempt(refresh: boolean = false) {
// This value should be relatively static or at least not break according to spec
// https://docs.solana.com/developing/programming-model/accounts#rent-exemption
if (!this._accountRentExempt || refresh) {
this._accountRentExempt = await this.connection.getMinimumBalanceForRentExemption(
AccountLayout.span
);
}
return this._accountRentExempt;
}
/**
* Retrieve a cached whirlpool account. Fetch from rpc on cache miss.
*
* @param address whirlpool address
* @param refresh force cache refresh
* @returns whirlpool account
*/
public async getPool(address: Address, refresh = false): Promise<WhirlpoolData | null> {
return this.get(AddressUtil.toPubKey(address), ParsableWhirlpool, refresh);
}
/**
* Retrieve a cached position account. Fetch from rpc on cache miss.
*
* @param address position address
* @param refresh force cache refresh
* @returns position account
*/
public async getPosition(address: Address, refresh = false): Promise<PositionData | null> {
return this.get(AddressUtil.toPubKey(address), ParsablePosition, refresh);
}
/**
* Retrieve a cached tick array account. Fetch from rpc on cache miss.
*
* @param address tick array address
* @param refresh force cache refresh
* @returns tick array account
*/
public async getTickArray(address: Address, refresh = false): Promise<TickArrayData | null> {
return this.get(AddressUtil.toPubKey(address), ParsableTickArray, refresh);
}
/**
* Retrieve a cached fee tier account. Fetch from rpc on cache miss.
*
* @param address fee tier address
* @param refresh force cache refresh
* @returns fee tier account
*/
public async getFeeTier(address: Address, refresh = false): Promise<FeeTierData | null> {
return this.get(AddressUtil.toPubKey(address), ParsableFeeTier, refresh);
}
/**
* Retrieve a cached token info account. Fetch from rpc on cache miss.
*
* @param address token info address
* @param refresh force cache refresh
* @returns token info account
*/
public async getTokenInfo(address: Address, refresh = false): Promise<AccountInfo | null> {
return this.get(AddressUtil.toPubKey(address), ParsableTokenInfo, refresh);
}
/**
* Retrieve a cached mint info account. Fetch from rpc on cache miss.
*
* @param address mint info address
* @param refresh force cache refresh
* @returns mint info account
*/
public async getMintInfo(address: Address, refresh = false): Promise<MintInfo | null> {
return this.get(AddressUtil.toPubKey(address), ParsableMintInfo, refresh);
}
/**
* Retrieve a cached whirlpool config account. Fetch from rpc on cache miss.
*
* @param address whirlpool config address
* @param refresh force cache refresh
* @returns whirlpool config account
*/
public async getConfig(address: Address, refresh = false): Promise<WhirlpoolsConfigData | null> {
return this.get(AddressUtil.toPubKey(address), ParsableWhirlpoolsConfig, refresh);
}
/**
* Retrieve a list of cached whirlpool accounts. Fetch from rpc for cache misses.
*
* @param addresses whirlpool addresses
* @param refresh force cache refresh
* @returns whirlpool accounts
*/
public async listPools(
addresses: Address[],
refresh: boolean
): Promise<(WhirlpoolData | null)[]> {
return this.list(AddressUtil.toPubKeys(addresses), ParsableWhirlpool, refresh);
}
/**
* Retrieve a list of cached position accounts. Fetch from rpc for cache misses.
*
* @param addresses position addresses
* @param refresh force cache refresh
* @returns position accounts
*/
public async listPositions(
addresses: Address[],
refresh: boolean
): Promise<(PositionData | null)[]> {
return this.list(AddressUtil.toPubKeys(addresses), ParsablePosition, refresh);
}
/**
* Retrieve a list of cached tick array accounts. Fetch from rpc for cache misses.
*
* @param addresses tick array addresses
* @param refresh force cache refresh
* @returns tick array accounts
*/
public async listTickArrays(
addresses: Address[],
refresh: boolean
): Promise<(TickArrayData | null)[]> {
return this.list(AddressUtil.toPubKeys(addresses), ParsableTickArray, refresh);
}
/**
* Retrieve a list of cached token info accounts. Fetch from rpc for cache misses.
*
* @param addresses token info addresses
* @param refresh force cache refresh
* @returns token info accounts
*/
public async listTokenInfos(
addresses: Address[],
refresh: boolean
): Promise<(AccountInfo | null)[]> {
return this.list(AddressUtil.toPubKeys(addresses), ParsableTokenInfo, refresh);
}
/**
* Retrieve a list of cached mint info accounts. Fetch from rpc for cache misses.
*
* @param addresses mint info addresses
* @param refresh force cache refresh
* @returns mint info accounts
*/
public async listMintInfos(addresses: Address[], refresh: boolean): Promise<(MintInfo | null)[]> {
return this.list(AddressUtil.toPubKeys(addresses), ParsableMintInfo, refresh);
}
/**
* Update the cached value of all entities currently in the cache.
* Uses batched rpc request for network efficient fetch.
*/
public async refreshAll(): Promise<void> {
const addresses: string[] = Object.keys(this._cache);
const data = await this.bulkRequest(addresses);
for (const [idx, [key, cachedContent]] of Object.entries(this._cache).entries()) {
const entity = cachedContent.entity;
const value = entity.parse(data[idx]);
this._cache[key] = { entity, value };
}
}
/*** Private Methods ***/
/**
* Retrieve from cache or fetch from rpc, an account
*/
private async get<T extends CachedValue>(
address: PublicKey,
entity: ParsableEntity<T>,
refresh: boolean
): Promise<T | null> {
const key = address.toBase58();
const cachedValue: CachedValue | null | undefined = this._cache[key]?.value;
if (cachedValue !== undefined && !refresh) {
return cachedValue as T | null;
}
const accountInfo = await this.connection.getAccountInfo(address);
const accountData = accountInfo?.data;
const value = entity.parse(accountData);
this._cache[key] = { entity, value };
return value;
}
/**
* Retrieve from cache or fetch from rpc, a list of accounts
*/
private async list<T extends CachedValue>(
addresses: PublicKey[],
entity: ParsableEntity<T>,
refresh: boolean
): Promise<(T | null)[]> {
const keys = addresses.map((address) => address.toBase58());
const cachedValues: [string, CachedValue | null | undefined][] = keys.map((key) => [
key,
refresh ? undefined : this._cache[key]?.value,
]);
/* Look for accounts not found in cache */
const undefinedAccounts: { cacheIndex: number; key: string }[] = [];
cachedValues.forEach(([key, value], cacheIndex) => {
if (value === undefined) {
undefinedAccounts.push({ cacheIndex, key });
}
});
/* Fetch accounts not found in cache */
if (undefinedAccounts.length > 0) {
const data = await this.bulkRequest(undefinedAccounts.map((account) => account.key));
undefinedAccounts.forEach(({ cacheIndex, key }, dataIndex) => {
const value = entity.parse(data[dataIndex]);
invariant(cachedValues[cacheIndex]?.[1] === undefined, "unexpected non-undefined value");
cachedValues[cacheIndex] = [key, value];
this._cache[key] = { entity, value };
});
}
const result = cachedValues
.map(([_, value]) => value)
.filter((value): value is T | null => value !== undefined);
invariant(result.length === addresses.length, "not enough results fetched");
return result;
}
/**
* Make batch rpc request
*/
private async bulkRequest(addresses: string[]): Promise<(Buffer | null)[]> {
const responses: Promise<GetMultipleAccountsResponse>[] = [];
const chunk = 100; // getMultipleAccounts has limitation of 100 accounts per request
for (let i = 0; i < addresses.length; i += chunk) {
const addressesSubset = addresses.slice(i, i + chunk);
const res = (this.connection as any)._rpcRequest("getMultipleAccounts", [
addressesSubset,
{ commitment: this.connection.commitment },
]);
responses.push(res);
}
const combinedResult: (Buffer | null)[] = [];
(await Promise.all(responses)).forEach((res) => {
invariant(!res.error, `bulkRequest result error: ${res.error}`);
invariant(!!res.result?.value, "bulkRequest no value");
res.result.value.forEach((account) => {
if (!account || account.data[1] !== "base64") {
combinedResult.push(null);
} else {
combinedResult.push(Buffer.from(account.data[0], account.data[1]));
}
});
});
invariant(combinedResult.length === addresses.length, "bulkRequest not enough results");
return combinedResult;
}
}

View File

@ -0,0 +1,2 @@
export * from "./fetcher";
export * from "./parsing";

View File

@ -0,0 +1,212 @@
import { AccountInfo, MintInfo, MintLayout, u64 } from "@solana/spl-token";
import { PublicKey } from "@solana/web3.js";
import {
WhirlpoolsConfigData,
WhirlpoolData,
PositionData,
TickArrayData,
AccountName,
FeeTierData,
} from "../../types/public";
import { AccountsCoder, Coder, Idl } from "@project-serum/anchor";
import * as WhirlpoolIDL from "../../artifacts/whirlpool.json";
import { TokenUtil } from "@orca-so/common-sdk";
/**
* Static abstract class definition to parse entities.
* @category Parsables
*/
export interface ParsableEntity<T> {
/**
* Parse account data
*
* @param accountData Buffer data for the entity
* @returns Parsed entity
*/
parse: (accountData: Buffer | undefined | null) => T | null;
}
/**
* @category Parsables
*/
@staticImplements<ParsableEntity<WhirlpoolsConfigData>>()
export class ParsableWhirlpoolsConfig {
private constructor() {}
public static parse(data: Buffer | undefined | null): WhirlpoolsConfigData | null {
if (!data) {
return null;
}
try {
return parseAnchorAccount(AccountName.WhirlpoolsConfig, data);
} catch (e) {
console.error(`error while parsing WhirlpoolsConfig: ${e}`);
return null;
}
}
}
/**
* @category Parsables
*/
@staticImplements<ParsableEntity<WhirlpoolData>>()
export class ParsableWhirlpool {
private constructor() {}
public static parse(data: Buffer | undefined | null): WhirlpoolData | null {
if (!data) {
return null;
}
try {
return parseAnchorAccount(AccountName.Whirlpool, data);
} catch (e) {
console.error(`error while parsing Whirlpool: ${e}`);
return null;
}
}
}
/**
* @category Parsables
*/
@staticImplements<ParsableEntity<PositionData>>()
export class ParsablePosition {
private constructor() {}
public static parse(data: Buffer | undefined | null): PositionData | null {
if (!data) {
return null;
}
try {
return parseAnchorAccount(AccountName.Position, data);
} catch (e) {
console.error(`error while parsing Position: ${e}`);
return null;
}
}
}
/**
* @category Parsables
*/
@staticImplements<ParsableEntity<TickArrayData>>()
export class ParsableTickArray {
private constructor() {}
public static parse(data: Buffer | undefined | null): TickArrayData | null {
if (!data) {
return null;
}
try {
return parseAnchorAccount(AccountName.TickArray, data);
} catch (e) {
console.error(`error while parsing TickArray: ${e}`);
return null;
}
}
}
/**
* @category Parsables
*/
@staticImplements<ParsableEntity<FeeTierData>>()
export class ParsableFeeTier {
private constructor() {}
public static parse(data: Buffer | undefined | null): FeeTierData | null {
if (!data) {
return null;
}
try {
return parseAnchorAccount(AccountName.FeeTier, data);
} catch (e) {
console.error(`error while parsing FeeTier: ${e}`);
return null;
}
}
}
/**
* @category Parsables
*/
@staticImplements<ParsableEntity<AccountInfo>>()
export class ParsableTokenInfo {
private constructor() {}
public static parse(data: Buffer | undefined | null): AccountInfo | null {
if (!data) {
return null;
}
try {
return TokenUtil.deserializeTokenAccount(data);
} catch (e) {
console.error(`error while parsing TokenAccount: ${e}`);
return null;
}
}
}
/**
* @category Parsables
*/
@staticImplements<ParsableEntity<MintInfo>>()
export class ParsableMintInfo {
private constructor() {}
public static parse(data: Buffer | undefined | null): MintInfo | null {
if (!data) {
return null;
}
try {
const buffer = MintLayout.decode(data);
const mintInfo: MintInfo = {
mintAuthority:
buffer.mintAuthorityOption === 0 ? null : new PublicKey(buffer.mintAuthority),
supply: u64.fromBuffer(buffer.supply),
decimals: buffer.decimals,
isInitialized: buffer.isInitialized !== 0,
freezeAuthority:
buffer.freezeAuthority === 0 ? null : new PublicKey(buffer.freezeAuthority),
};
return mintInfo;
} catch (e) {
console.error(`error while parsing MintInfo: ${e}`);
return null;
}
}
}
/**
* Class decorator to define an interface with static methods
* Reference: https://github.com/Microsoft/TypeScript/issues/13462#issuecomment-295685298
*/
function staticImplements<T>() {
return <U extends T>(constructor: U) => {
constructor;
};
}
const WhirlpoolCoder = new Coder(WhirlpoolIDL as Idl);
function parseAnchorAccount(accountName: AccountName, data: Buffer) {
const discriminator = AccountsCoder.accountDiscriminator(accountName);
if (discriminator.compare(data.slice(0, 8))) {
console.error("incorrect account name during parsing");
return null;
}
try {
return WhirlpoolCoder.accounts.decode(accountName, data);
} catch (_e) {
console.error("unknown account name during parsing");
return null;
}
}

View File

@ -0,0 +1,116 @@
import { MathUtil } from "@orca-so/common-sdk";
import { BN } from "@project-serum/anchor";
import { PositionData, TickData, WhirlpoolData } from "../../types/public";
/**
* @category Quotes
*/
export type CollectFeesQuoteParam = {
whirlpool: WhirlpoolData;
position: PositionData;
tickLower: TickData;
tickUpper: TickData;
};
/**
* @category Quotes
*/
export type CollectFeesQuote = {
feeOwedA: BN;
feeOwedB: BN;
};
/**
* Get a quote on the outstanding fees owed to a position.
*
* @category Quotes
* @param param A collection of fetched Whirlpool accounts to faciliate the quote.
* @returns A quote object containing the fees owed for each token in the pool.
*/
export function collectFeesQuote(param: CollectFeesQuoteParam): CollectFeesQuote {
const { whirlpool, position, tickLower, tickUpper } = param;
const {
tickCurrentIndex,
feeGrowthGlobalA: feeGrowthGlobalAX64,
feeGrowthGlobalB: feeGrowthGlobalBX64,
} = whirlpool;
const {
tickLowerIndex,
tickUpperIndex,
liquidity,
feeOwedA,
feeOwedB,
feeGrowthCheckpointA: feeGrowthCheckpointAX64,
feeGrowthCheckpointB: feeGrowthCheckpointBX64,
} = position;
const {
feeGrowthOutsideA: tickLowerFeeGrowthOutsideAX64,
feeGrowthOutsideB: tickLowerFeeGrowthOutsideBX64,
} = tickLower;
const {
feeGrowthOutsideA: tickUpperFeeGrowthOutsideAX64,
feeGrowthOutsideB: tickUpperFeeGrowthOutsideBX64,
} = tickUpper;
// Calculate the fee growths inside the position
let feeGrowthBelowAX64: BN | null = null;
let feeGrowthBelowBX64: BN | null = null;
if (tickCurrentIndex < tickLowerIndex) {
feeGrowthBelowAX64 = MathUtil.subUnderflowU128(
feeGrowthGlobalAX64,
tickLowerFeeGrowthOutsideAX64
);
feeGrowthBelowBX64 = MathUtil.subUnderflowU128(
feeGrowthGlobalBX64,
tickLowerFeeGrowthOutsideBX64
);
} else {
feeGrowthBelowAX64 = tickLowerFeeGrowthOutsideAX64;
feeGrowthBelowBX64 = tickLowerFeeGrowthOutsideBX64;
}
let feeGrowthAboveAX64: BN | null = null;
let feeGrowthAboveBX64: BN | null = null;
if (tickCurrentIndex < tickUpperIndex) {
feeGrowthAboveAX64 = tickUpperFeeGrowthOutsideAX64;
feeGrowthAboveBX64 = tickUpperFeeGrowthOutsideBX64;
} else {
feeGrowthAboveAX64 = MathUtil.subUnderflowU128(
feeGrowthGlobalAX64,
tickUpperFeeGrowthOutsideAX64
);
feeGrowthAboveBX64 = MathUtil.subUnderflowU128(
feeGrowthGlobalBX64,
tickUpperFeeGrowthOutsideBX64
);
}
const feeGrowthInsideAX64 = MathUtil.subUnderflowU128(
MathUtil.subUnderflowU128(feeGrowthGlobalAX64, feeGrowthBelowAX64),
feeGrowthAboveAX64
);
const feeGrowthInsideBX64 = MathUtil.subUnderflowU128(
MathUtil.subUnderflowU128(feeGrowthGlobalBX64, feeGrowthBelowBX64),
feeGrowthAboveBX64
);
// Calculate the updated fees owed
const feeOwedADelta = MathUtil.subUnderflowU128(feeGrowthInsideAX64, feeGrowthCheckpointAX64)
.mul(liquidity)
.shrn(64);
const feeOwedBDelta = MathUtil.subUnderflowU128(feeGrowthInsideBX64, feeGrowthCheckpointBX64)
.mul(liquidity)
.shrn(64);
const updatedFeeOwedA = feeOwedA.add(feeOwedADelta);
const updatedFeeOwedB = feeOwedB.add(feeOwedBDelta);
return {
feeOwedA: updatedFeeOwedA,
feeOwedB: updatedFeeOwedB,
};
}

View File

@ -0,0 +1,125 @@
import { MathUtil } from "@orca-so/common-sdk";
import { BN } from "@project-serum/anchor";
import invariant from "tiny-invariant";
import { NUM_REWARDS, PositionData, TickData, WhirlpoolData } from "../../types/public";
import { PoolUtil } from "../../utils/public/pool-utils";
/**
* @category Quotes
*/
export type CollectRewardsQuoteParam = {
whirlpool: WhirlpoolData;
position: PositionData;
tickLower: TickData;
tickUpper: TickData;
};
/**
* @category Quotes
*/
export type CollectRewardsQuote = [BN | undefined, BN | undefined, BN | undefined];
/**
* Get a quote on the outstanding rewards owed to a position.
*
* @category Quotes
* @param param A collection of fetched Whirlpool accounts to faciliate the quote.
* @returns A quote object containing the rewards owed for each reward in the pool.
*/
export function collectRewardsQuote(param: CollectRewardsQuoteParam): CollectRewardsQuote {
const { whirlpool, position, tickLower, tickUpper } = param;
const { tickCurrentIndex, rewardInfos: whirlpoolRewardsInfos } = whirlpool;
const { tickLowerIndex, tickUpperIndex, liquidity, rewardInfos } = position;
// Calculate the reward growths inside the position
const range = [...Array(NUM_REWARDS).keys()];
const rewardGrowthsBelowX64: BN[] = range.map(() => new BN(0));
const rewardGrowthsAboveX64: BN[] = range.map(() => new BN(0));
for (const i of range) {
const rewardInfo = whirlpoolRewardsInfos[i];
invariant(!!rewardInfo, "whirlpoolRewardsInfos cannot be undefined");
const growthGlobalX64 = rewardInfo.growthGlobalX64;
const lowerRewardGrowthsOutside = tickLower.rewardGrowthsOutside[i];
const upperRewardGrowthsOutside = tickUpper.rewardGrowthsOutside[i];
invariant(!!lowerRewardGrowthsOutside, "lowerRewardGrowthsOutside cannot be undefined");
invariant(!!upperRewardGrowthsOutside, "upperRewardGrowthsOutside cannot be undefined");
if (tickCurrentIndex < tickLowerIndex) {
rewardGrowthsBelowX64[i] = MathUtil.subUnderflowU128(
growthGlobalX64,
lowerRewardGrowthsOutside
);
} else {
rewardGrowthsBelowX64[i] = lowerRewardGrowthsOutside;
}
if (tickCurrentIndex < tickUpperIndex) {
rewardGrowthsAboveX64[i] = upperRewardGrowthsOutside;
} else {
rewardGrowthsAboveX64[i] = MathUtil.subUnderflowU128(
growthGlobalX64,
upperRewardGrowthsOutside
);
}
}
const rewardGrowthsInsideX64: [BN, boolean][] = range.map(() => [new BN(0), false]);
for (const i of range) {
const rewardInfo = whirlpoolRewardsInfos[i];
invariant(!!rewardInfo, "whirlpoolRewardsInfos cannot be undefined");
const isRewardInitialized = PoolUtil.isRewardInitialized(rewardInfo);
if (isRewardInitialized) {
const growthBelowX64 = rewardGrowthsBelowX64[i];
const growthAboveX64 = rewardGrowthsAboveX64[i];
invariant(!!growthBelowX64, "growthBelowX64 cannot be undefined");
invariant(!!growthAboveX64, "growthAboveX64 cannot be undefined");
const growthInsde = MathUtil.subUnderflowU128(
MathUtil.subUnderflowU128(rewardInfo.growthGlobalX64, growthBelowX64),
growthAboveX64
);
rewardGrowthsInsideX64[i] = [growthInsde, true];
}
}
// Calculate the updated rewards owed
const updatedRewardInfosX64: BN[] = range.map(() => new BN(0));
for (const i of range) {
const growthInsideX64 = rewardGrowthsInsideX64[i];
invariant(!!growthInsideX64, "growthInsideX64 cannot be undefined");
const [rewardGrowthInsideX64, isRewardInitialized] = growthInsideX64;
if (isRewardInitialized) {
const rewardInfo = rewardInfos[i];
invariant(!!rewardInfo, "rewardInfo cannot be undefined");
const amountOwedX64 = rewardInfo.amountOwed.shln(64);
const growthInsideCheckpointX64 = rewardInfo.growthInsideCheckpoint;
updatedRewardInfosX64[i] = amountOwedX64.add(
MathUtil.subUnderflowU128(rewardGrowthInsideX64, growthInsideCheckpointX64).mul(liquidity)
);
}
}
invariant(rewardGrowthsInsideX64.length >= 3, "rewards length is less than 3");
const rewardExistsA = rewardGrowthsInsideX64[0]?.[1];
const rewardExistsB = rewardGrowthsInsideX64[1]?.[1];
const rewardExistsC = rewardGrowthsInsideX64[2]?.[1];
const rewardOwedA = rewardExistsA ? updatedRewardInfosX64[0]?.shrn(64) : undefined;
const rewardOwedB = rewardExistsB ? updatedRewardInfosX64[1]?.shrn(64) : undefined;
const rewardOwedC = rewardExistsC ? updatedRewardInfosX64[2]?.shrn(64) : undefined;
return [rewardOwedA, rewardOwedB, rewardOwedC];
}

View File

@ -0,0 +1,155 @@
import { Percentage } from "@orca-so/common-sdk";
import { ZERO } from "@orca-so/sdk";
import { BN } from "@project-serum/anchor";
import { u64 } from "@solana/spl-token";
import invariant from "tiny-invariant";
import { DecreaseLiquidityInput } from "../../instructions";
import {
PositionUtil,
PositionStatus,
adjustForSlippage,
getTokenAFromLiquidity,
getTokenBFromLiquidity,
} from "../../utils/position-util";
import { PriceMath, TickUtil } from "../../utils/public";
import { Position, Whirlpool } from "../../whirlpool-client";
/**
* @category Quotes
* @param liquidity - The desired liquidity to withdraw from the Whirlpool
* @param tickCurrentIndex - The Whirlpool's current tickIndex
* @param sqrtPrice - The Whirlpool's current sqrtPrice
* @param tickLowerIndex - The lower index of the position that we are withdrawing from.
* @param tickUpperIndex - The upper index of the position that we are withdrawing from.
* @param slippageTolerance - The maximum slippage allowed when calculating the minimum tokens received.
*/
export type DecreaseLiquidityQuoteParam = {
liquidity: u64;
tickCurrentIndex: number;
sqrtPrice: BN;
tickLowerIndex: number;
tickUpperIndex: number;
slippageTolerance: Percentage;
};
export async function decreaseLiquidityQuoteByLiquidity(
liquidity: u64,
slippageTolerance: Percentage,
position: Position,
whirlpool: Whirlpool
) {
const positionData = position.getData();
const whirlpoolData = whirlpool.getData();
invariant(
liquidity.lte(positionData.liquidity),
"Quote liquidity is more than the position liquidity."
);
return decreaseLiquidityQuoteByLiquidityWithParams({
liquidity,
slippageTolerance,
tickLowerIndex: positionData.tickLowerIndex,
tickUpperIndex: positionData.tickUpperIndex,
sqrtPrice: whirlpoolData.sqrtPrice,
tickCurrentIndex: whirlpoolData.tickCurrentIndex,
});
}
/**
* Get an estimated quote on the minimum tokens receivable based on the desired withdraw liquidity value.
*
* @category Quotes
* @param param DecreaseLiquidityQuoteParam
* @returns An DecreaseLiquidityInput object detailing the tokenMin & liquidity values to use when calling decrease-liquidity-ix.
*/
export function decreaseLiquidityQuoteByLiquidityWithParams(
param: DecreaseLiquidityQuoteParam
): DecreaseLiquidityInput {
invariant(TickUtil.checkTickInBounds(param.tickLowerIndex), "tickLowerIndex is out of bounds.");
invariant(TickUtil.checkTickInBounds(param.tickUpperIndex), "tickUpperIndex is out of bounds.");
invariant(
TickUtil.checkTickInBounds(param.tickCurrentIndex),
"tickCurrentIndex is out of bounds."
);
const positionStatus = PositionUtil.getPositionStatus(
param.tickCurrentIndex,
param.tickLowerIndex,
param.tickUpperIndex
);
switch (positionStatus) {
case PositionStatus.BelowRange:
return quotePositionBelowRange(param);
case PositionStatus.InRange:
return quotePositionInRange(param);
case PositionStatus.AboveRange:
return quotePositionAboveRange(param);
default:
throw new Error(`type ${positionStatus} is an unknown PositionStatus`);
}
}
function quotePositionBelowRange(param: DecreaseLiquidityQuoteParam): DecreaseLiquidityInput {
const { tickLowerIndex, tickUpperIndex, liquidity, slippageTolerance } = param;
const sqrtPriceLowerX64 = PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex);
const sqrtPriceUpperX64 = PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex);
const minTokenA = adjustForSlippage(
getTokenAFromLiquidity(liquidity, sqrtPriceLowerX64, sqrtPriceUpperX64, false),
slippageTolerance,
false
);
return {
tokenMinA: minTokenA,
tokenMinB: ZERO,
liquidityAmount: liquidity,
};
}
function quotePositionInRange(param: DecreaseLiquidityQuoteParam): DecreaseLiquidityInput {
const { sqrtPrice, tickLowerIndex, tickUpperIndex, liquidity, slippageTolerance } = param;
const sqrtPriceX64 = sqrtPrice;
const sqrtPriceLowerX64 = PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex);
const sqrtPriceUpperX64 = PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex);
const minTokenA = adjustForSlippage(
getTokenAFromLiquidity(liquidity, sqrtPriceX64, sqrtPriceUpperX64, false),
slippageTolerance,
false
);
const minTokenB = adjustForSlippage(
getTokenBFromLiquidity(liquidity, sqrtPriceLowerX64, sqrtPriceX64, false),
slippageTolerance,
false
);
return {
tokenMinA: minTokenA,
tokenMinB: minTokenB,
liquidityAmount: liquidity,
};
}
function quotePositionAboveRange(param: DecreaseLiquidityQuoteParam): DecreaseLiquidityInput {
const { tickLowerIndex, tickUpperIndex, liquidity, slippageTolerance: slippageTolerance } = param;
const sqrtPriceLowerX64 = PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex);
const sqrtPriceUpperX64 = PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex);
const minTokenB = adjustForSlippage(
getTokenBFromLiquidity(liquidity, sqrtPriceLowerX64, sqrtPriceUpperX64, false),
slippageTolerance,
false
);
return {
tokenMinA: ZERO,
tokenMinB: minTokenB,
liquidityAmount: liquidity,
};
}

View File

@ -0,0 +1,230 @@
import { DecimalUtil, Percentage, ZERO } from "@orca-so/common-sdk";
import { BN } from "@project-serum/anchor";
import { u64 } from "@solana/spl-token";
import { PublicKey } from "@solana/web3.js";
import Decimal from "decimal.js";
import invariant from "tiny-invariant";
import { IncreaseLiquidityInput } from "../../instructions";
import { TokenInfo, WhirlpoolData } from "../../types/public";
import {
PositionUtil,
PositionStatus,
getLiquidityFromTokenA,
adjustForSlippage,
getTokenAFromLiquidity,
getTokenBFromLiquidity,
getLiquidityFromTokenB,
} from "../../utils/position-util";
import { PriceMath, TickUtil } from "../../utils/public";
import { Whirlpool } from "../../whirlpool-client";
/**
* @category Quotes
* @param inputTokenAmount - The amount of input tokens to deposit.
* @param inputTokenMint - The mint of the input token the user would like to deposit.
* @param tokenMintA - The mint of tokenA in the Whirlpool the user is depositing into.
* @param tokenMintB -The mint of tokenB in the Whirlpool the user is depositing into.
* @param tickCurrentIndex - The Whirlpool's current tickIndex
* @param sqrtPrice - The Whirlpool's current sqrtPrice
* @param tickLowerIndex - The lower index of the position that we are withdrawing from.
* @param tickUpperIndex - The upper index of the position that we are withdrawing from.
* @param slippageTolerance - The maximum slippage allowed when calculating the minimum tokens received.
*/
export type IncreaseLiquidityQuoteParam = {
inputTokenAmount: u64;
inputTokenMint: PublicKey;
tokenMintA: PublicKey;
tokenMintB: PublicKey;
tickCurrentIndex: number;
sqrtPrice: BN;
tickLowerIndex: number;
tickUpperIndex: number;
slippageTolerance: Percentage;
};
export function increaseLiquidityQuoteByInputToken(
inputTokenMint: PublicKey,
inputTokenAmount: Decimal,
tickLower: number,
tickUpper: number,
slippageTolerance: Percentage,
whirlpool: Whirlpool
) {
const data = whirlpool.getData();
const tokenAInfo = whirlpool.getTokenAInfo();
const tokenBInfo = whirlpool.getTokenBInfo();
const inputTokenInfo = inputTokenMint.equals(tokenAInfo.mint) ? tokenAInfo : tokenBInfo;
return increaseLiquidityQuoteByInputTokenWithParams({
inputTokenMint: inputTokenMint,
inputTokenAmount: DecimalUtil.toU64(inputTokenAmount, inputTokenInfo.decimals),
tickLowerIndex: TickUtil.getInitializableTickIndex(tickLower, data.tickSpacing),
tickUpperIndex: TickUtil.getInitializableTickIndex(tickUpper, data.tickSpacing),
slippageTolerance,
...data,
});
}
/**
* Get an estimated quote on the maximum tokens required to deposit based on a specified input token amount.
*
* @category Quotes
* @param param IncreaseLiquidityQuoteParam
* @returns An IncreaseLiquidityInput object detailing the required token amounts & liquidity values to use when calling increase-liquidity-ix.
*/
export function increaseLiquidityQuoteByInputTokenWithParams(
param: IncreaseLiquidityQuoteParam
): IncreaseLiquidityInput {
invariant(TickUtil.checkTickInBounds(param.tickLowerIndex), "tickLowerIndex is out of bounds.");
invariant(TickUtil.checkTickInBounds(param.tickUpperIndex), "tickUpperIndex is out of bounds.");
invariant(
param.inputTokenMint.equals(param.tokenMintA) || param.inputTokenMint.equals(param.tokenMintB),
`input token mint ${param.inputTokenMint.toBase58()} does not match any tokens in the provided pool.`
);
const positionStatus = PositionUtil.getPositionStatus(
param.tickCurrentIndex,
param.tickLowerIndex,
param.tickUpperIndex
);
switch (positionStatus) {
case PositionStatus.BelowRange:
return quotePositionBelowRange(param);
case PositionStatus.InRange:
return quotePositionInRange(param);
case PositionStatus.AboveRange:
return quotePositionAboveRange(param);
default:
throw new Error(`type ${positionStatus} is an unknown PositionStatus`);
}
}
/*** Private ***/
function quotePositionBelowRange(param: IncreaseLiquidityQuoteParam): IncreaseLiquidityInput {
const {
tokenMintA,
inputTokenMint,
inputTokenAmount,
tickLowerIndex,
tickUpperIndex,
slippageTolerance,
} = param;
if (!tokenMintA.equals(inputTokenMint)) {
return {
tokenMaxA: ZERO,
tokenMaxB: ZERO,
liquidityAmount: ZERO,
};
}
const sqrtPriceLowerX64 = PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex);
const sqrtPriceUpperX64 = PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex);
const liquidityAmount = getLiquidityFromTokenA(
inputTokenAmount,
sqrtPriceLowerX64,
sqrtPriceUpperX64,
false
);
const maxTokenA = adjustForSlippage(
getTokenAFromLiquidity(liquidityAmount, sqrtPriceLowerX64, sqrtPriceUpperX64, true),
slippageTolerance,
true
);
const maxTokenB = ZERO;
return {
tokenMaxA: maxTokenA,
tokenMaxB: maxTokenB,
liquidityAmount,
};
}
function quotePositionInRange(param: IncreaseLiquidityQuoteParam): IncreaseLiquidityInput {
const {
tokenMintA,
sqrtPrice,
inputTokenMint,
inputTokenAmount,
tickLowerIndex,
tickUpperIndex,
slippageTolerance,
} = param;
const sqrtPriceX64 = sqrtPrice;
const sqrtPriceLowerX64 = PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex);
const sqrtPriceUpperX64 = PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex);
let [tokenAmountA, tokenAmountB] = tokenMintA.equals(inputTokenMint)
? [inputTokenAmount, undefined]
: [undefined, inputTokenAmount];
let liquidityAmount: BN;
if (tokenAmountA) {
liquidityAmount = getLiquidityFromTokenA(tokenAmountA, sqrtPriceX64, sqrtPriceUpperX64, false);
tokenAmountA = getTokenAFromLiquidity(liquidityAmount, sqrtPriceX64, sqrtPriceUpperX64, true);
tokenAmountB = getTokenBFromLiquidity(liquidityAmount, sqrtPriceLowerX64, sqrtPriceX64, true);
} else if (tokenAmountB) {
liquidityAmount = getLiquidityFromTokenB(tokenAmountB, sqrtPriceLowerX64, sqrtPriceX64, false);
tokenAmountA = getTokenAFromLiquidity(liquidityAmount, sqrtPriceX64, sqrtPriceUpperX64, true);
tokenAmountB = getTokenBFromLiquidity(liquidityAmount, sqrtPriceLowerX64, sqrtPriceX64, true);
} else {
throw new Error("invariant violation");
}
const maxTokenA = adjustForSlippage(tokenAmountA, slippageTolerance, true);
const maxTokenB = adjustForSlippage(tokenAmountB, slippageTolerance, true);
return {
tokenMaxA: maxTokenA,
tokenMaxB: maxTokenB,
liquidityAmount,
};
}
function quotePositionAboveRange(param: IncreaseLiquidityQuoteParam): IncreaseLiquidityInput {
const {
tokenMintB,
inputTokenMint,
inputTokenAmount,
tickLowerIndex,
tickUpperIndex,
slippageTolerance,
} = param;
if (!tokenMintB.equals(inputTokenMint)) {
return {
tokenMaxA: ZERO,
tokenMaxB: ZERO,
liquidityAmount: ZERO,
};
}
const sqrtPriceLowerX64 = PriceMath.tickIndexToSqrtPriceX64(tickLowerIndex);
const sqrtPriceUpperX64 = PriceMath.tickIndexToSqrtPriceX64(tickUpperIndex);
const liquidityAmount = getLiquidityFromTokenB(
inputTokenAmount,
sqrtPriceLowerX64,
sqrtPriceUpperX64,
false
);
const maxTokenA = ZERO;
const maxTokenB = adjustForSlippage(
getTokenBFromLiquidity(liquidityAmount, sqrtPriceLowerX64, sqrtPriceUpperX64, true),
slippageTolerance,
true
);
return {
tokenMaxA: maxTokenA,
tokenMaxB: maxTokenB,
liquidityAmount,
};
}

View File

@ -0,0 +1,5 @@
export * from "./increase-liquidity-quote";
export * from "./decrease-liquidity-quote";
export * from "./collect-fees-quote";
export * from "./collect-rewards-quote";
export * from "./swap-quote";

View File

@ -0,0 +1,439 @@
import { Address, BN } from "@project-serum/anchor";
import { PublicKey } from "@solana/web3.js";
import { u64 } from "@solana/spl-token";
import invariant from "tiny-invariant";
import { PoolUtil } from "../../utils/public/pool-utils";
import {
SwapDirection,
AmountSpecified,
adjustAmountForSlippage,
getAmountFixedDelta,
getNextSqrtPrice,
getAmountUnfixedDelta,
} from "../../utils/position-util";
import { SwapInput } from "../../instructions";
import {
WhirlpoolData,
TickArrayData,
MAX_TICK_ARRAY_CROSSINGS,
MIN_SQRT_PRICE,
MAX_SQRT_PRICE,
TICK_ARRAY_SIZE,
} from "../../types/public";
import { AddressUtil, MathUtil, Percentage, ZERO } from "@orca-so/common-sdk";
import { PriceMath, TickArrayUtil, TickUtil } from "../../utils/public";
/**
* @category Quotes
*/
export type SwapQuoteParam = {
whirlpoolAddress: Address;
swapTokenMint: Address;
whirlpoolData: WhirlpoolData;
tokenAmount: u64;
amountSpecifiedIsInput: boolean;
slippageTolerance: Percentage;
tickArrayAddresses: PublicKey[];
tickArrays: (TickArrayData | null)[];
};
/**
* @category Quotes
*/
export type SwapQuote = {
estimatedAmountIn: u64;
estimatedAmountOut: u64;
} & SwapInput;
/**
* TODO: Bug - The quote swap loop will ignore the first initialized tick of the next array on array traversal
* if the tick is on offset 0.
* TODO: Build out a comprhensive integ test-suite for all tick traversal. The test suite would confirm
* edge cases for both smart-contract and quote (they must equal). The original test-cases in SDK can be ported
* over in this effort.
* TODO: Think about other types of quote that we want to write (ex. limit by price to get trade amount etc)
*
* Get an estimated quote of a swap
*
* @category Quotes
* @param param a SwapQuoteParam object detailing parameters of the swap
* @return a SwapQuote on the estimated amountIn & amountOut of the swap and a SwapInput to use on the swap instruction.
*/
export function swapQuoteByInputToken(param: SwapQuoteParam): SwapQuote {
const {
whirlpoolAddress,
swapTokenMint,
tokenAmount,
whirlpoolData,
amountSpecifiedIsInput,
slippageTolerance,
tickArrays,
tickArrayAddresses,
} = param;
const swapDirection =
AddressUtil.toPubKey(swapTokenMint).equals(whirlpoolData.tokenMintA) === amountSpecifiedIsInput
? SwapDirection.AtoB
: SwapDirection.BtoA;
const amountSpecified = amountSpecifiedIsInput ? AmountSpecified.Input : AmountSpecified.Output;
const { amountIn, amountOut, sqrtPriceLimitX64, tickArraysCrossed } = simulateSwap(
{
whirlpoolAddress,
whirlpoolData,
amountSpecified,
swapDirection,
tickArrays,
},
{
amount: tokenAmount,
currentSqrtPriceX64: whirlpoolData.sqrtPrice,
currentTickIndex: whirlpoolData.tickCurrentIndex,
currentLiquidity: whirlpoolData.liquidity,
}
);
const otherAmountThreshold = adjustAmountForSlippage(
amountIn,
amountOut,
slippageTolerance,
amountSpecified
);
// Compute the traversed set of tick-arrays. Set the remaining slots to
//the last traversed array.
const traversedTickArrays = tickArrayAddresses.map((addr, index) => {
if (index < tickArraysCrossed) {
return addr;
}
return tickArrayAddresses[Math.max(tickArraysCrossed - 1, 0)];
});
return {
amount: tokenAmount,
otherAmountThreshold,
sqrtPriceLimit: sqrtPriceLimitX64,
estimatedAmountIn: amountIn,
estimatedAmountOut: amountOut,
aToB: swapDirection === SwapDirection.AtoB,
amountSpecifiedIsInput,
tickArray0: traversedTickArrays[0],
tickArray1: traversedTickArrays[1],
tickArray2: traversedTickArrays[2],
};
}
type SwapSimulationBaseInput = {
whirlpoolAddress: Address;
whirlpoolData: WhirlpoolData;
amountSpecified: AmountSpecified;
swapDirection: SwapDirection;
tickArrays: (TickArrayData | null)[];
};
type SwapSimulationInput = {
amount: BN;
currentSqrtPriceX64: BN;
currentTickIndex: number;
currentLiquidity: BN;
};
type SwapSimulationOutput = {
amountIn: BN;
amountOut: BN;
sqrtPriceLimitX64: BN;
tickArraysCrossed: number;
};
type SwapStepSimulationInput = {
sqrtPriceX64: BN;
tickIndex: number;
liquidity: BN;
amountRemaining: u64;
tickArraysCrossed: number;
};
type SwapStepSimulationOutput = {
nextSqrtPriceX64: BN;
nextTickIndex: number;
input: BN;
output: BN;
tickArraysCrossed: number;
hasReachedNextTick: boolean;
};
function simulateSwap(
baseInput: SwapSimulationBaseInput,
input: SwapSimulationInput
): SwapSimulationOutput {
const { amountSpecified, swapDirection } = baseInput;
let {
currentTickIndex,
currentLiquidity,
amount: specifiedAmountLeft,
currentSqrtPriceX64,
} = input;
invariant(!specifiedAmountLeft.eq(ZERO), "amount must be nonzero");
let otherAmountCalculated = ZERO;
let tickArraysCrossed = 0;
let sqrtPriceLimitX64;
while (specifiedAmountLeft.gt(ZERO)) {
if (tickArraysCrossed > MAX_TICK_ARRAY_CROSSINGS) {
throw Error("Crossed the maximum number of tick arrays");
}
const swapStepSimulationOutput: SwapStepSimulationOutput = simulateSwapStep(baseInput, {
sqrtPriceX64: currentSqrtPriceX64,
amountRemaining: specifiedAmountLeft,
tickIndex: currentTickIndex,
liquidity: currentLiquidity,
tickArraysCrossed,
});
const { input, output, nextSqrtPriceX64, nextTickIndex, hasReachedNextTick } =
swapStepSimulationOutput;
const [specifiedAmountUsed, otherAmount] = resolveTokenAmounts(input, output, amountSpecified);
specifiedAmountLeft = specifiedAmountLeft.sub(specifiedAmountUsed);
otherAmountCalculated = otherAmountCalculated.add(otherAmount);
if (hasReachedNextTick) {
const nextTick = fetchTick(baseInput, nextTickIndex);
currentLiquidity = calculateNewLiquidity(
currentLiquidity,
nextTick.liquidityNet,
swapDirection
);
currentTickIndex = swapDirection == SwapDirection.AtoB ? nextTickIndex - 1 : nextTickIndex;
}
currentSqrtPriceX64 = nextSqrtPriceX64;
tickArraysCrossed = swapStepSimulationOutput.tickArraysCrossed;
if (tickArraysCrossed > MAX_TICK_ARRAY_CROSSINGS) {
sqrtPriceLimitX64 = PriceMath.tickIndexToSqrtPriceX64(nextTickIndex);
}
}
const [inputAmount, outputAmount] = resolveTokenAmounts(
input.amount.sub(specifiedAmountLeft),
otherAmountCalculated,
amountSpecified
);
if (!sqrtPriceLimitX64) {
if (swapDirection === SwapDirection.AtoB) {
sqrtPriceLimitX64 = new BN(MIN_SQRT_PRICE);
} else {
sqrtPriceLimitX64 = new BN(MAX_SQRT_PRICE);
}
}
// Return sqrtPriceLimit if 3 tick arrays crossed
return {
amountIn: inputAmount,
amountOut: outputAmount,
sqrtPriceLimitX64,
tickArraysCrossed,
};
}
function simulateSwapStep(
baseInput: SwapSimulationBaseInput,
input: SwapStepSimulationInput
): SwapStepSimulationOutput {
const { whirlpoolData, amountSpecified, swapDirection } = baseInput;
const { feeRate } = whirlpoolData;
const feeRatePercentage = PoolUtil.getFeeRate(feeRate);
const { amountRemaining, liquidity, sqrtPriceX64, tickIndex, tickArraysCrossed } = input;
const { tickIndex: nextTickIndex, tickArraysCrossed: tickArraysCrossedUpdate } =
// Return last tick in tick array if max tick arrays crossed
// Error out of this gets called for another iteration
getNextInitializedTickIndex(baseInput, tickIndex, tickArraysCrossed);
const targetSqrtPriceX64 = PriceMath.tickIndexToSqrtPriceX64(nextTickIndex);
let fixedDelta = getAmountFixedDelta(
sqrtPriceX64,
targetSqrtPriceX64,
liquidity,
amountSpecified,
swapDirection
);
let amountCalculated = amountRemaining;
if (amountSpecified == AmountSpecified.Input) {
amountCalculated = calculateAmountAfterFees(amountRemaining, feeRatePercentage);
}
const nextSqrtPriceX64 = amountCalculated.gte(fixedDelta)
? targetSqrtPriceX64 // Fully utilize liquidity till upcoming (next/prev depending on swap type) initialized tick
: getNextSqrtPrice(sqrtPriceX64, liquidity, amountCalculated, amountSpecified, swapDirection);
const hasReachedNextTick = nextSqrtPriceX64.eq(targetSqrtPriceX64);
const unfixedDelta = getAmountUnfixedDelta(
sqrtPriceX64,
nextSqrtPriceX64,
liquidity,
amountSpecified,
swapDirection
);
if (!hasReachedNextTick) {
fixedDelta = getAmountFixedDelta(
sqrtPriceX64,
nextSqrtPriceX64,
liquidity,
amountSpecified,
swapDirection
);
}
let [inputDelta, outputDelta] = resolveTokenAmounts(fixedDelta, unfixedDelta, amountSpecified);
// Cap output if output specified
if (amountSpecified == AmountSpecified.Output && outputDelta.gt(amountRemaining)) {
outputDelta = amountRemaining;
}
if (amountSpecified == AmountSpecified.Input && !hasReachedNextTick) {
inputDelta = amountRemaining;
} else {
inputDelta = inputDelta.add(calculateFeesFromAmount(inputDelta, feeRatePercentage));
}
return {
nextTickIndex,
nextSqrtPriceX64,
input: inputDelta,
output: outputDelta,
tickArraysCrossed: tickArraysCrossedUpdate,
hasReachedNextTick,
};
}
function calculateAmountAfterFees(amount: u64, feeRate: Percentage): BN {
return amount.mul(feeRate.denominator.sub(feeRate.numerator)).div(feeRate.denominator);
}
function calculateFeesFromAmount(amount: u64, feeRate: Percentage): BN {
return MathUtil.divRoundUp(
amount.mul(feeRate.numerator),
feeRate.denominator.sub(feeRate.numerator)
);
}
function calculateNewLiquidity(liquidity: BN, nextLiquidityNet: BN, swapDirection: SwapDirection) {
if (swapDirection == SwapDirection.AtoB) {
nextLiquidityNet = nextLiquidityNet.neg();
}
return liquidity.add(nextLiquidityNet);
}
function resolveTokenAmounts(
specifiedTokenAmount: BN,
otherTokenAmount: BN,
amountSpecified: AmountSpecified
): [BN, BN] {
if (amountSpecified == AmountSpecified.Input) {
return [specifiedTokenAmount, otherTokenAmount];
} else {
return [otherTokenAmount, specifiedTokenAmount];
}
}
function fetchTickArray(baseInput: SwapSimulationBaseInput, tickIndex: number) {
const {
tickArrays,
whirlpoolData: { tickSpacing },
} = baseInput;
const startTickArray = tickArrays[0];
invariant(!!startTickArray, `tickArray is null at index 0`);
const sequenceStartIndex = startTickArray.startTickIndex;
const expectedArrayIndex = Math.abs(
Math.floor((tickIndex - sequenceStartIndex) / tickSpacing / TICK_ARRAY_SIZE)
);
invariant(
expectedArrayIndex > 0 || expectedArrayIndex < tickArrays.length,
`tickIndex ${tickIndex} assumes array-index of ${expectedArrayIndex} and is out of bounds for sequence`
);
const tickArray = tickArrays[expectedArrayIndex];
invariant(!!tickArray, `tickArray is null at array-index ${expectedArrayIndex}`);
return tickArray;
}
function fetchTick(baseInput: SwapSimulationBaseInput, tickIndex: number) {
const tickArray = fetchTickArray(baseInput, tickIndex);
const {
whirlpoolData: { tickSpacing },
} = baseInput;
return TickArrayUtil.getTickFromArray(tickArray, tickIndex, tickSpacing);
}
function getNextInitializedTickIndex(
baseInput: SwapSimulationBaseInput,
currentTickIndex: number,
tickArraysCrossed: number
) {
const {
whirlpoolData: { tickSpacing },
swapDirection,
} = baseInput;
let nextInitializedTickIndex: number | undefined = undefined;
while (nextInitializedTickIndex === undefined) {
const currentTickArray = fetchTickArray(baseInput, currentTickIndex);
let temp;
if (swapDirection == SwapDirection.AtoB) {
temp = TickUtil.findPreviousInitializedTickIndex(
currentTickArray,
currentTickIndex,
tickSpacing
);
} else {
temp = TickUtil.findNextInitializedTickIndex(currentTickArray, currentTickIndex, tickSpacing);
}
if (temp) {
nextInitializedTickIndex = temp;
} else if (tickArraysCrossed === MAX_TICK_ARRAY_CROSSINGS) {
if (swapDirection === SwapDirection.AtoB) {
nextInitializedTickIndex = currentTickArray.startTickIndex;
} else {
nextInitializedTickIndex = currentTickArray.startTickIndex + TICK_ARRAY_SIZE * tickSpacing;
}
tickArraysCrossed++;
} else {
if (swapDirection === SwapDirection.AtoB) {
currentTickIndex = currentTickArray.startTickIndex - 1;
} else {
currentTickIndex = currentTickArray.startTickIndex + TICK_ARRAY_SIZE * tickSpacing - 1;
}
tickArraysCrossed++;
}
}
return {
tickIndex: nextInitializedTickIndex,
tickArraysCrossed,
};
}

View File

@ -1,10 +1,11 @@
{
"extends": "../tsconfig-base.json",
"compilerOptions": {
"composite": true,
"composite": true
},
"include": [
"./**/*",
"./artifacts/**/*.json"
]
"include": ["./**/*", "./artifacts/**/*.json"],
"typedocOptions": {
"entryPoints": ["index.ts"],
"out": "../../target/typedocs"
}
}

View File

@ -1,16 +0,0 @@
import { PublicKey } from "@solana/web3.js";
/**
* This file contains the types that the client exposes to SDK users.
*
* TODO: This file may or may not exist pending SDK's approach on parsing
* the Whirlpool Accounts.
*
*/
export interface WhirlpoolConfigAccount {
feeAuthority: PublicKey;
collectProtocolFeesAuthority: PublicKey;
rewardEmissionsSuperAuthority: PublicKey;
defaultFeeRate: number;
defaultProtocolFeeRate: number;
}

View File

@ -9,18 +9,32 @@ import { PublicKey } from "@solana/web3.js";
* be hard-casted to fit the type.
*/
/**
* Supported parasable account names from the Whirlpool contract.
* @category Parsables
*/
export enum AccountName {
WhirlpoolsConfig = "WhirlpoolsConfig",
Position = "Position",
TickArray = "TickArray",
Whirlpool = "Whirlpool",
FeeTier = "FeeTier",
}
export type TickSpacingData = {
stable?: {};
standard?: {};
/**
* @category Solana Accounts
*/
export type WhirlpoolsConfigData = {
feeAuthority: PublicKey;
collectProtocolFeesAuthority: PublicKey;
rewardEmissionsSuperAuthority: PublicKey;
defaultFeeRate: number;
defaultProtocolFeeRate: number;
};
/**
* @category Solana Accounts
*/
export type WhirlpoolRewardInfoData = {
mint: PublicKey;
vault: PublicKey;
@ -29,10 +43,16 @@ export type WhirlpoolRewardInfoData = {
growthGlobalX64: BN;
};
/**
* @category Solana Accounts
*/
export type WhirlpoolBumpsData = {
whirlpoolBump: number;
};
/**
* @category Solana Accounts
*/
export type WhirlpoolData = {
whirlpoolsConfig: PublicKey;
whirlpoolBump: number[];
@ -54,12 +74,18 @@ export type WhirlpoolData = {
tickSpacing: number;
};
/**
* @category Solana Accounts
*/
export type TickArrayData = {
whirlpool: PublicKey;
startTickIndex: number;
ticks: TickData[];
};
/**
* @category Solana Accounts
*/
export type TickData = {
initialized: boolean;
liquidityNet: BN;
@ -69,20 +95,32 @@ export type TickData = {
rewardGrowthsOutside: BN[];
};
/**
* @category Solana Accounts
*/
export type PositionRewardInfoData = {
growthInsideCheckpoint: BN;
amountOwed: BN;
};
/**
* @category Solana Accounts
*/
export type OpenPositionBumpsData = {
positionBump: number;
};
/**
* @category Solana Accounts
*/
export type OpenPositionWithMetadataBumpsData = {
positionBump: number;
metadataBump: number;
};
/**
* @category Solana Accounts
*/
export type PositionData = {
whirlpool: PublicKey;
positionMint: PublicKey;
@ -96,6 +134,9 @@ export type PositionData = {
rewardInfos: PositionRewardInfoData[];
};
/**
* @category Solana Accounts
*/
export type FeeTierData = {
whirlpoolsConfig: PublicKey;
tickSpacing: number;

View File

@ -0,0 +1,4 @@
import { PublicKey } from "@solana/web3.js";
import { MintInfo } from "@solana/spl-token";
export type TokenInfo = MintInfo & { mint: PublicKey };

View File

@ -1,2 +1,44 @@
// The number of rewards supported by this whirlpool.
import { PublicKey } from "@solana/web3.js";
/**
* The number of rewards supported by this whirlpool.
* @category Constants
*/
export const NUM_REWARDS = 3;
/**
* @category Constants
*/
export const MAX_TICK_ARRAY_CROSSINGS = 2;
/**
* @category Constants
*/
export const MAX_TICK_INDEX = 443636;
/**
* @category Constants
*/
export const MIN_TICK_INDEX = -443636;
/**
* @category Constants
*/
export const TICK_ARRAY_SIZE = 88;
/**
* @category Constants
*/
export const MAX_SQRT_PRICE = "79226673515401279992447579055";
/**
* @category Constants
*/
export const MIN_SQRT_PRICE = "4295048016";
/**
* @category Constants
*/
export const METADATA_PROGRAM_ADDRESS = new PublicKey(
"metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
);

View File

@ -1,3 +0,0 @@
import { PublicKey } from "@solana/web3.js";
export type PDA = { publicKey: PublicKey; bump: number };

View File

@ -1,5 +1,4 @@
export * from "./ix-types";
export * from "./account-types";
export * from "./constants";
export * from "./helper-types";
export * from "./anchor-types";
export * from "./ix-types";
export * from "./client-types";

View File

@ -1,227 +1,29 @@
import { u64 } from "@solana/spl-token";
import { Keypair, PublicKey } from "@solana/web3.js";
import BN from "bn.js";
import { PDA } from "./helper-types";
export type InitConfigParams = {
whirlpoolConfigKeypair: Keypair;
feeAuthority: PublicKey;
collectProtocolFeesAuthority: PublicKey;
rewardEmissionsSuperAuthority: PublicKey;
defaultProtocolFeeRate: number;
funder: PublicKey;
};
export type InitFeeTierParams = {
feeTierPda: PDA;
feeAuthority: PublicKey;
whirlpoolConfigKey: PublicKey;
tickSpacing: number;
defaultFeeRate: number;
funder: PublicKey;
};
export type InitPoolParams = {
initSqrtPrice: BN;
whirlpoolConfigKey: PublicKey;
tokenMintA: PublicKey;
tokenMintB: PublicKey;
whirlpoolPda: PDA;
feeTierKey: PublicKey;
tokenVaultAKeypair: Keypair;
tokenVaultBKeypair: Keypair;
tickSpacing: number;
funder: PublicKey;
};
export type InitTickArrayParams = {
whirlpool: PublicKey;
tickArrayPda: PDA;
startTick: number;
funder: PublicKey;
};
export type InitializeRewardParams = {
rewardAuthority: PublicKey;
funder: PublicKey;
whirlpool: PublicKey;
rewardMint: PublicKey;
rewardVaultKeypair: Keypair;
rewardIndex: number;
};
export type SetRewardEmissionsParams = {
rewardAuthority: PublicKey;
whirlpool: PublicKey;
rewardIndex: number;
rewardVault: PublicKey;
emissionsPerSecondX64: BN;
};
export type OpenPositionParams = {
funder: PublicKey;
ownerKey: PublicKey;
positionPda: PDA;
metadataPda?: PDA;
positionMintAddress: PublicKey;
positionTokenAccountAddress: PublicKey;
whirlpoolKey: PublicKey;
tickLowerIndex: number;
tickUpperIndex: number;
};
export type ClosePositionParams = {
positionAuthority: PublicKey;
receiver: PublicKey;
position: PublicKey;
positionMint: PublicKey;
positionTokenAccount: PublicKey;
};
export type IncreaseLiquidityParams = {
liquidityAmount: BN;
tokenMaxA: u64;
tokenMaxB: u64;
whirlpool: PublicKey;
positionAuthority: PublicKey;
position: PublicKey;
positionTokenAccount: PublicKey;
tokenOwnerAccountA: PublicKey;
tokenOwnerAccountB: PublicKey;
tokenVaultA: PublicKey;
tokenVaultB: PublicKey;
tickArrayLower: PublicKey;
tickArrayUpper: PublicKey;
};
export type DecreaseLiquidityParams = {
liquidityAmount: BN;
tokenMinA: u64;
tokenMinB: u64;
whirlpool: PublicKey;
positionAuthority: PublicKey;
position: PublicKey;
positionTokenAccount: PublicKey;
tokenOwnerAccountA: PublicKey;
tokenOwnerAccountB: PublicKey;
tokenVaultA: PublicKey;
tokenVaultB: PublicKey;
tickArrayLower: PublicKey;
tickArrayUpper: PublicKey;
};
export type UpdateFeesAndRewardsParams = {
whirlpool: PublicKey;
position: PublicKey;
tickArrayLower: PublicKey;
tickArrayUpper: PublicKey;
};
export type CollectFeesParams = {
whirlpool: PublicKey;
positionAuthority: PublicKey;
position: PublicKey;
positionTokenAccount: PublicKey;
tokenOwnerAccountA: PublicKey;
tokenOwnerAccountB: PublicKey;
tokenVaultA: PublicKey;
tokenVaultB: PublicKey;
};
export type CollectRewardParams = {
whirlpool: PublicKey;
positionAuthority: PublicKey;
position: PublicKey;
positionTokenAccount: PublicKey;
rewardOwnerAccount: PublicKey;
rewardVault: PublicKey;
rewardIndex: number;
};
export type CollectProtocolFeesParams = {
whirlpoolsConfig: PublicKey;
whirlpool: PublicKey;
collectProtocolFeesAuthority: PublicKey;
tokenVaultA: PublicKey;
tokenVaultB: PublicKey;
tokenDestinationA: PublicKey;
tokenDestinationB: PublicKey;
};
export type SwapParams = {
amount: u64;
otherAmountThreshold: u64;
sqrtPriceLimit: BN;
amountSpecifiedIsInput: boolean;
aToB: boolean;
whirlpool: PublicKey;
tokenAuthority: PublicKey;
tokenOwnerAccountA: PublicKey;
tokenVaultA: PublicKey;
tokenOwnerAccountB: PublicKey;
tokenVaultB: PublicKey;
tickArray0: PublicKey;
tickArray1: PublicKey;
tickArray2: PublicKey;
oracle: PublicKey;
};
export type SetRewardEmissionsSuperAuthorityParams = {
whirlpoolsConfig: PublicKey;
rewardEmissionsSuperAuthority: PublicKey;
newRewardEmissionsSuperAuthority: PublicKey;
};
export type SetRewardAuthorityParams = {
whirlpool: PublicKey;
rewardAuthority: PublicKey;
newRewardAuthority: PublicKey;
rewardIndex: number;
};
export type SetRewardAuthorityBySuperAuthorityParams = {
whirlpoolsConfig: PublicKey;
whirlpool: PublicKey;
rewardEmissionsSuperAuthority: PublicKey;
newRewardAuthority: PublicKey;
rewardIndex: number;
};
export type SetFeeAuthorityParams = {
whirlpoolsConfig: PublicKey;
feeAuthority: PublicKey;
newFeeAuthority: PublicKey;
};
export type SetCollectProtocolFeesAuthorityParams = {
whirlpoolsConfig: PublicKey;
collectProtocolFeesAuthority: PublicKey;
newCollectProtocolFeesAuthority: PublicKey;
};
export type SetFeeRateParams = {
whirlpool: PublicKey;
whirlpoolsConfig: PublicKey;
feeAuthority: PublicKey;
feeRate: number;
};
export type SetProtocolFeeRateParams = {
whirlpool: PublicKey;
whirlpoolsConfig: PublicKey;
feeAuthority: PublicKey;
protocolFeeRate: number;
};
export type SetDefaultFeeRateParams = {
whirlpoolsConfig: PublicKey;
feeAuthority: PublicKey;
tickSpacing: number;
defaultFeeRate: number;
};
export type SetDefaultProtocolFeeRateParams = {
whirlpoolsConfig: PublicKey;
feeAuthority: PublicKey;
defaultProtocolFeeRate: number;
};
export {
CollectFeesParams,
ClosePositionParams,
CollectProtocolFeesParams,
CollectRewardParams,
SetCollectProtocolFeesAuthorityParams,
SetDefaultFeeRateParams,
SetDefaultProtocolFeeRateParams,
SetFeeAuthorityParams,
SetFeeRateParams,
SetProtocolFeeRateParams,
SetRewardAuthorityBySuperAuthorityParams,
SetRewardAuthorityParams,
SetRewardEmissionsParams,
SetRewardEmissionsSuperAuthorityParams,
IncreaseLiquidityParams,
IncreaseLiquidityInput,
DecreaseLiquidityInput,
DecreaseLiquidityParams,
OpenPositionParams,
SwapInput,
SwapParams,
UpdateFeesAndRewardsParams,
InitConfigParams,
InitFeeTierParams,
InitPoolParams,
InitTickArrayParams,
InitializeRewardParams,
} from "../../instructions/";

View File

@ -1,8 +0,0 @@
import { utils } from "@project-serum/anchor";
import { PublicKey } from "@solana/web3.js";
import { PDA } from "../types/public/helper-types";
export function findProgramAddress(seeds: (Uint8Array | Buffer)[], programId: PublicKey): PDA {
const [publicKey, bump] = utils.publicKey.findProgramAddressSync(seeds, programId);
return { publicKey, bump };
}

View File

@ -0,0 +1,33 @@
import { TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { OpenPositionParams } from "../instructions";
import * as anchor from "@project-serum/anchor";
import { SystemProgram } from "@solana/web3.js";
import { WhirlpoolContext } from "../context";
import { Instruction, TransactionBuilder } from "@orca-so/common-sdk";
export function toTx(ctx: WhirlpoolContext, ix: Instruction): TransactionBuilder {
return new TransactionBuilder(ctx.provider).addInstruction(ix);
}
export function openPositionAccounts(params: OpenPositionParams) {
const {
funder,
owner,
positionPda,
positionMintAddress,
positionTokenAccount: positionTokenAccountAddress,
whirlpool: whirlpoolKey,
} = params;
return {
funder: funder,
owner,
position: positionPda.publicKey,
positionMint: positionMintAddress,
positionTokenAccount: positionTokenAccountAddress,
whirlpool: whirlpoolKey,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
};
}

View File

@ -0,0 +1,206 @@
import { MathUtil, Percentage } from "@orca-so/common-sdk";
import { BN } from "@project-serum/anchor";
import {
getLowerSqrtPriceFromTokenA,
getUpperSqrtPriceFromTokenA,
getUpperSqrtPriceFromTokenB,
getLowerSqrtPriceFromTokenB,
} from "./swap-utils";
export enum SwapDirection {
AtoB = "Swap A to B",
BtoA = "Swap B to A",
}
export enum AmountSpecified {
Input = "Specified input amount",
Output = "Specified output amount",
}
export enum PositionStatus {
BelowRange,
InRange,
AboveRange,
}
export class PositionUtil {
private constructor() {}
public static getPositionStatus(
tickCurrentIndex: number,
tickLowerIndex: number,
tickUpperIndex: number
): PositionStatus {
if (tickCurrentIndex < tickLowerIndex) {
return PositionStatus.BelowRange;
} else if (tickCurrentIndex < tickUpperIndex) {
return PositionStatus.InRange;
} else {
return PositionStatus.AboveRange;
}
}
}
export function adjustForSlippage(
n: BN,
{ numerator, denominator }: Percentage,
adjustUp: boolean
): BN {
if (adjustUp) {
return n.mul(denominator.add(numerator)).div(denominator);
} else {
return n.mul(denominator).div(denominator.add(numerator));
}
}
export function adjustAmountForSlippage(
amountIn: BN,
amountOut: BN,
{ numerator, denominator }: Percentage,
amountSpecified: AmountSpecified
): BN {
if (amountSpecified === AmountSpecified.Input) {
return amountOut.mul(denominator).div(denominator.add(numerator));
} else {
return amountIn.mul(denominator.add(numerator)).div(denominator);
}
}
export function getLiquidityFromTokenA(
amount: BN,
sqrtPriceLowerX64: BN,
sqrtPriceUpperX64: BN,
roundUp: boolean
) {
const result = amount
.mul(sqrtPriceLowerX64)
.mul(sqrtPriceUpperX64)
.div(sqrtPriceUpperX64.sub(sqrtPriceLowerX64));
if (roundUp) {
return MathUtil.shiftRightRoundUp(result);
} else {
return result.shrn(64);
}
}
export function getLiquidityFromTokenB(
amount: BN,
sqrtPriceLowerX64: BN,
sqrtPriceUpperX64: BN,
roundUp: boolean
) {
const numerator = amount.shln(64);
const denominator = sqrtPriceUpperX64.sub(sqrtPriceLowerX64);
if (roundUp) {
return MathUtil.divRoundUp(numerator, denominator);
} else {
return numerator.div(denominator);
}
}
function orderSqrtPrice(sqrtPrice0X64: BN, sqrtPrice1X64: BN): [BN, BN] {
if (sqrtPrice0X64.lt(sqrtPrice1X64)) {
return [sqrtPrice0X64, sqrtPrice1X64];
} else {
return [sqrtPrice1X64, sqrtPrice0X64];
}
}
export function getTokenAFromLiquidity(
liquidity: BN,
sqrtPrice0X64: BN,
sqrtPrice1X64: BN,
roundUp: boolean
) {
const [sqrtPriceLowerX64, sqrtPriceUpperX64] = orderSqrtPrice(sqrtPrice0X64, sqrtPrice1X64);
const numerator = liquidity.mul(sqrtPriceUpperX64.sub(sqrtPriceLowerX64)).shln(64);
const denominator = sqrtPriceUpperX64.mul(sqrtPriceLowerX64);
if (roundUp) {
return MathUtil.divRoundUp(numerator, denominator);
} else {
return numerator.div(denominator);
}
}
export function getTokenBFromLiquidity(
liquidity: BN,
sqrtPrice0X64: BN,
sqrtPrice1X64: BN,
roundUp: boolean
) {
const [sqrtPriceLowerX64, sqrtPriceUpperX64] = orderSqrtPrice(sqrtPrice0X64, sqrtPrice1X64);
const result = liquidity.mul(sqrtPriceUpperX64.sub(sqrtPriceLowerX64));
if (roundUp) {
return MathUtil.shiftRightRoundUp(result);
} else {
return result.shrn(64);
}
}
export function getAmountFixedDelta(
currentSqrtPriceX64: BN,
targetSqrtPriceX64: BN,
liquidity: BN,
amountSpecified: AmountSpecified,
swapDirection: SwapDirection
) {
if ((amountSpecified == AmountSpecified.Input) == (swapDirection == SwapDirection.AtoB)) {
return getTokenAFromLiquidity(
liquidity,
currentSqrtPriceX64,
targetSqrtPriceX64,
amountSpecified == AmountSpecified.Input
);
} else {
return getTokenBFromLiquidity(
liquidity,
currentSqrtPriceX64,
targetSqrtPriceX64,
amountSpecified == AmountSpecified.Input
);
}
}
export function getAmountUnfixedDelta(
currentSqrtPriceX64: BN,
targetSqrtPriceX64: BN,
liquidity: BN,
amountSpecified: AmountSpecified,
swapDirection: SwapDirection
) {
if ((amountSpecified == AmountSpecified.Input) == (swapDirection == SwapDirection.AtoB)) {
return getTokenBFromLiquidity(
liquidity,
currentSqrtPriceX64,
targetSqrtPriceX64,
amountSpecified == AmountSpecified.Output
);
} else {
return getTokenAFromLiquidity(
liquidity,
currentSqrtPriceX64,
targetSqrtPriceX64,
amountSpecified == AmountSpecified.Output
);
}
}
export function getNextSqrtPrice(
sqrtPriceX64: BN,
liquidity: BN,
amount: BN,
amountSpecified: AmountSpecified,
swapDirection: SwapDirection
) {
if (amountSpecified === AmountSpecified.Input && swapDirection === SwapDirection.AtoB) {
return getLowerSqrtPriceFromTokenA(amount, liquidity, sqrtPriceX64);
} else if (amountSpecified === AmountSpecified.Output && swapDirection === SwapDirection.BtoA) {
return getUpperSqrtPriceFromTokenA(amount, liquidity, sqrtPriceX64);
} else if (amountSpecified === AmountSpecified.Input && swapDirection === SwapDirection.BtoA) {
return getUpperSqrtPriceFromTokenB(amount, liquidity, sqrtPriceX64);
} else {
return getLowerSqrtPriceFromTokenB(amount, liquidity, sqrtPriceX64);
}
}

View File

@ -1,108 +0,0 @@
import { PublicKey } from "@solana/web3.js";
import { findProgramAddress } from "../find-program-address";
import { BN } from "@project-serum/anchor";
const PDA_WHIRLPOOL_SEED = "whirlpool";
const PDA_VAULT_A_SEED = "token_vault_a";
const PDA_VAULT_B_SEED = "token_vault_b";
const PDA_POSITION_SEED = "position";
const PDA_METADATA_SEED = "metadata";
const PDA_TICK_ARRAY_SEED = "tick_array";
const PDA_FEE_TIER_SEED = "fee_tier";
const PDA_ORACLE_SEED = "oracle";
export function getWhirlpoolPda(
programId: PublicKey,
whirlpoolConfigKey: PublicKey,
tokenMintAKey: PublicKey,
tokenMintBKey: PublicKey,
tickSpacing: number
) {
return findProgramAddress(
[
Buffer.from(PDA_WHIRLPOOL_SEED),
whirlpoolConfigKey.toBuffer(),
tokenMintAKey.toBuffer(),
tokenMintBKey.toBuffer(),
new BN(tickSpacing).toArrayLike(Buffer, "le", 2),
],
programId
);
}
export function getWhirlpoolVaultAPda(
programId: PublicKey,
whirlpoolKey: PublicKey,
tokenMintAKey: PublicKey
) {
return findProgramAddress(
[Buffer.from(PDA_VAULT_A_SEED), whirlpoolKey.toBuffer(), tokenMintAKey.toBuffer()],
programId
);
}
export function getWhirlpoolVaultBPda(
programId: PublicKey,
whirlpoolKey: PublicKey,
tokenMintBKey: PublicKey
) {
return findProgramAddress(
[Buffer.from(PDA_VAULT_B_SEED), whirlpoolKey.toBuffer(), tokenMintBKey.toBuffer()],
programId
);
}
export function getPositionPda(programId: PublicKey, positionMintKey: PublicKey) {
return findProgramAddress(
[Buffer.from(PDA_POSITION_SEED), positionMintKey.toBuffer()],
programId
);
}
export const METADATA_PROGRAM_ADDRESS = new PublicKey(
"metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
);
export function getPositionMetadataPda(positionMintKey: PublicKey) {
return findProgramAddress(
[
Buffer.from(PDA_METADATA_SEED),
METADATA_PROGRAM_ADDRESS.toBuffer(),
positionMintKey.toBuffer(),
],
METADATA_PROGRAM_ADDRESS
);
}
export function getTickArrayPda(
programId: PublicKey,
whirlpoolAddress: PublicKey,
startTick: number
) {
return findProgramAddress(
[
Buffer.from(PDA_TICK_ARRAY_SEED),
whirlpoolAddress.toBuffer(),
Buffer.from(startTick.toString()),
],
programId
);
}
export function getFeeTierPda(
programId: PublicKey,
whirlpoolsConfigAddress: PublicKey,
tickSpacing: number
) {
return findProgramAddress(
[
Buffer.from(PDA_FEE_TIER_SEED),
whirlpoolsConfigAddress.toBuffer(),
new BN(tickSpacing).toArrayLike(Buffer, "le", 2),
],
programId
);
}
export function getOraclePda(programId: PublicKey, whirlpoolAddress: PublicKey) {
return findProgramAddress([Buffer.from(PDA_ORACLE_SEED), whirlpoolAddress.toBuffer()], programId);
}

View File

@ -1,5 +1,4 @@
export * from "./addresses";
export * from "./math";
export * from "./parse";
export * from "./pda-utils";
export * from "./price-math";
export * from "./tick-utils";
export * from "./liquidity";
export * from "./pool-utils";

View File

@ -1,134 +0,0 @@
import { BN } from "@project-serum/anchor";
import { u64 } from "@solana/spl-token";
import { min } from "bn.js";
import Decimal from "decimal.js";
import {
fromX64_BN,
fromX64_Decimal,
tickIndexToSqrtPriceX64,
toX64,
toX64_BN,
toX64_Decimal,
} from ".";
export type TokenAmounts = {
tokenA: u64;
tokenB: u64;
};
export function toTokenAmount(a: number, b: number): TokenAmounts {
return {
tokenA: new u64(a.toString()),
tokenB: new u64(b.toString()),
};
}
/**
* Estimate the liquidity amount required to increase/decrease liquidity.
*
* // TODO: At the top end of the price range, tick calcuation is off therefore the results can be off
*
* @param currTick - Whirlpool's current tick index (aka price)
* @param lowerTick - Position lower tick index
* @param upperTick - Position upper tick index
* @param tokenAmount - The desired amount of tokens to deposit/withdraw
* @returns An estimated amount of liquidity needed to deposit/withdraw the desired amount of tokens.
*/
export function estimateLiquidityFromTokenAmounts(
currTick: number,
lowerTick: number,
upperTick: number,
tokenAmount: TokenAmounts
): BN {
if (upperTick < lowerTick) {
throw new Error("upper tick cannot be lower than the lower tick");
}
const currSqrtPrice = tickIndexToSqrtPriceX64(currTick);
const lowerSqrtPrice = tickIndexToSqrtPriceX64(lowerTick);
const upperSqrtPrice = tickIndexToSqrtPriceX64(upperTick);
if (currTick >= upperTick) {
return estLiquidityForTokenB(upperSqrtPrice, lowerSqrtPrice, tokenAmount.tokenB);
} else if (currTick < lowerTick) {
return estLiquidityForTokenA(lowerSqrtPrice, upperSqrtPrice, tokenAmount.tokenA);
} else {
const estLiquidityAmountA = estLiquidityForTokenA(
currSqrtPrice,
upperSqrtPrice,
tokenAmount.tokenA
);
const estLiquidityAmountB = estLiquidityForTokenB(
currSqrtPrice,
lowerSqrtPrice,
tokenAmount.tokenB
);
return BN.min(estLiquidityAmountA, estLiquidityAmountB);
}
}
export function getTokenAmountsFromLiquidity(
liquidity: u64,
currentPrice: u64,
lowerPrice: u64,
upperPrice: u64,
round_up: boolean
): TokenAmounts {
const _liquidity = new Decimal(liquidity.toString());
const _currentPrice = new Decimal(currentPrice.toString());
const _lowerPrice = new Decimal(lowerPrice.toString());
const _upperPrice = new Decimal(upperPrice.toString());
let tokenA, tokenB;
if (currentPrice.lt(lowerPrice)) {
// x = L * (pb - pa) / (pa * pb)
tokenA = toX64_Decimal(_liquidity)
.mul(_upperPrice.sub(_lowerPrice))
.div(_lowerPrice.mul(_upperPrice));
tokenB = new Decimal(0);
} else if (currentPrice.lt(upperPrice)) {
// x = L * (pb - p) / (p * pb)
// y = L * (p - pa)
tokenA = toX64_Decimal(_liquidity)
.mul(_upperPrice.sub(_currentPrice))
.div(_currentPrice.mul(_upperPrice));
tokenB = fromX64_Decimal(_liquidity.mul(_currentPrice.sub(_lowerPrice)));
} else {
// y = L * (pb - pa)
tokenA = new Decimal(0);
tokenB = fromX64_Decimal(_liquidity.mul(_upperPrice.sub(_lowerPrice)));
}
// TODO: round up
if (round_up) {
return {
tokenA: new u64(tokenA.ceil().toString()),
tokenB: new u64(tokenB.ceil().toString()),
};
} else {
return {
tokenA: new u64(tokenA.floor().toString()),
tokenB: new u64(tokenB.floor().toString()),
};
}
}
// Convert this function based on Delta A = Delta L * (1/sqrt(lower) - 1/sqrt(upper))
function estLiquidityForTokenA(sqrtPrice1: BN, sqrtPrice2: BN, tokenAmount: u64) {
const lowerSqrtPriceX64 = BN.min(sqrtPrice1, sqrtPrice2);
const upperSqrtPriceX64 = BN.max(sqrtPrice1, sqrtPrice2);
const num = fromX64_BN(tokenAmount.mul(upperSqrtPriceX64).mul(lowerSqrtPriceX64));
const dem = upperSqrtPriceX64.sub(lowerSqrtPriceX64);
return num.div(dem);
}
// Convert this function based on Delta B = Delta L * (sqrt_price(upper) - sqrt_price(lower))
function estLiquidityForTokenB(sqrtPrice1: BN, sqrtPrice2: BN, tokenAmount: u64) {
const lowerSqrtPriceX64 = BN.min(sqrtPrice1, sqrtPrice2);
const upperSqrtPriceX64 = BN.max(sqrtPrice1, sqrtPrice2);
const delta = upperSqrtPriceX64.sub(lowerSqrtPriceX64);
return toX64_BN(tokenAmount).div(delta);
}

View File

@ -1,26 +0,0 @@
import { BN } from "@project-serum/anchor";
import Decimal from "decimal.js";
export function toX64_BN(num: BN): BN {
return num.mul(new BN(2).pow(new BN(64)));
}
export function toX64_Decimal(num: Decimal): Decimal {
return num.mul(Decimal.pow(2, 64));
}
export function toX64(num: Decimal): BN {
return new BN(num.mul(Decimal.pow(2, 64)).floor().toFixed());
}
export function fromX64(num: BN): Decimal {
return new Decimal(num.toString()).mul(Decimal.pow(2, -64));
}
export function fromX64_Decimal(num: Decimal): Decimal {
return num.mul(Decimal.pow(2, -64));
}
export function fromX64_BN(num: BN): BN {
return num.div(new BN(2).pow(new BN(64)));
}

View File

@ -1,42 +0,0 @@
import { AccountsCoder, Coder, Idl } from "@project-serum/anchor";
import { WhirlpoolConfigAccount } from "../..";
import {
AccountName,
PositionData,
TickArrayData,
WhirlpoolData,
} from "../../types/public/anchor-types";
import * as WhirlpoolIDL from "../../artifacts/whirlpool.json";
const WhirlpoolCoder = new Coder(WhirlpoolIDL as Idl);
export function parseWhirlpoolsConfig(data: Buffer): WhirlpoolConfigAccount | null {
return parse(AccountName.WhirlpoolsConfig, data);
}
export function parseWhirlpool(data: Buffer): WhirlpoolData | null {
return parse(AccountName.Whirlpool, data);
}
export function parsePosition(data: Buffer): PositionData | null {
return parse(AccountName.Position, data);
}
export function parseTickArray(data: Buffer): TickArrayData | null {
return parse(AccountName.TickArray, data);
}
function parse(accountName: AccountName, data: Buffer) {
const discriminator = AccountsCoder.accountDiscriminator(accountName);
if (discriminator.compare(data.slice(0, 8))) {
console.error("incorrect account name during parsing");
return null;
}
try {
return WhirlpoolCoder.accounts.decode(accountName, data);
} catch (_e) {
console.error("unknown account name during parsing");
return null;
}
}

View File

@ -0,0 +1,171 @@
import { AddressUtil } from "@orca-so/common-sdk";
import { BN } from "@project-serum/anchor";
import { PublicKey } from "@solana/web3.js";
import { METADATA_PROGRAM_ADDRESS } from "../../types/public";
import { PriceMath } from "./price-math";
import { TickUtil } from "./tick-utils";
const PDA_WHIRLPOOL_SEED = "whirlpool";
const PDA_POSITION_SEED = "position";
const PDA_METADATA_SEED = "metadata";
const PDA_TICK_ARRAY_SEED = "tick_array";
const PDA_FEE_TIER_SEED = "fee_tier";
const PDA_ORACLE_SEED = "oracle";
/**
* @category Program Derived Addresses
*/
export class PDAUtil {
/**
*
* @param programId
* @param whirlpoolsConfigKey
* @param tokenMintAKey
* @param tokenMintBKey
* @param tickSpacing
* @returns
*/
public static getWhirlpool(
programId: PublicKey,
whirlpoolsConfigKey: PublicKey,
tokenMintAKey: PublicKey,
tokenMintBKey: PublicKey,
tickSpacing: number
) {
return AddressUtil.findProgramAddress(
[
Buffer.from(PDA_WHIRLPOOL_SEED),
whirlpoolsConfigKey.toBuffer(),
tokenMintAKey.toBuffer(),
tokenMintBKey.toBuffer(),
new BN(tickSpacing).toArrayLike(Buffer, "le", 2),
],
programId
);
}
/**
* @category Program Derived Addresses
* @param programId
* @param positionMintKey
* @returns
*/
public static getPosition(programId: PublicKey, positionMintKey: PublicKey) {
return AddressUtil.findProgramAddress(
[Buffer.from(PDA_POSITION_SEED), positionMintKey.toBuffer()],
programId
);
}
/**
* @category Program Derived Addresses
* @param positionMintKey
* @returns
*/
public static getPositionMetadata(positionMintKey: PublicKey) {
return AddressUtil.findProgramAddress(
[
Buffer.from(PDA_METADATA_SEED),
METADATA_PROGRAM_ADDRESS.toBuffer(),
positionMintKey.toBuffer(),
],
METADATA_PROGRAM_ADDRESS
);
}
/**
* @category Program Derived Addresses
* @param programId
* @param whirlpoolAddress
* @param startTick
* @returns
*/
public static getTickArray(programId: PublicKey, whirlpoolAddress: PublicKey, startTick: number) {
return AddressUtil.findProgramAddress(
[
Buffer.from(PDA_TICK_ARRAY_SEED),
whirlpoolAddress.toBuffer(),
Buffer.from(startTick.toString()),
],
programId
);
}
/**
* Get the PDA of the tick array containing tickIndex.
* tickArrayOffset can be used to get neighboring tick arrays.
*
* @param tickIndex
* @param tickSpacing
* @param whirlpool
* @param programId
* @param tickArrayOffset
* @returns
*/
public static getTickArrayFromTickIndex(
tickIndex: number,
tickSpacing: number,
whirlpool: PublicKey,
programId: PublicKey,
tickArrayOffset = 0
) {
const startIndex = TickUtil.getStartTickIndex(tickIndex, tickSpacing, tickArrayOffset);
return PDAUtil.getTickArray(
AddressUtil.toPubKey(programId),
AddressUtil.toPubKey(whirlpool),
startIndex
);
}
public static getTickArrayFromSqrtPrice(
sqrtPriceX64: BN,
tickSpacing: number,
whirlpool: PublicKey,
programId: PublicKey,
tickArrayOffset = 0
) {
const tickIndex = PriceMath.sqrtPriceX64ToTickIndex(sqrtPriceX64);
return PDAUtil.getTickArrayFromTickIndex(
tickIndex,
tickSpacing,
whirlpool,
programId,
tickArrayOffset
);
}
/**
* @category Program Derived Addresses
* @param programId
* @param whirlpoolsConfigAddress
* @param tickSpacing
* @returns
*/
public static getFeeTier(
programId: PublicKey,
whirlpoolsConfigAddress: PublicKey,
tickSpacing: number
) {
return AddressUtil.findProgramAddress(
[
Buffer.from(PDA_FEE_TIER_SEED),
whirlpoolsConfigAddress.toBuffer(),
new BN(tickSpacing).toArrayLike(Buffer, "le", 2),
],
programId
);
}
/**
* @category Program Derived Addresses
* @param programId
* @param whirlpoolAddress
* @returns
*/
public static getOracle(programId: PublicKey, whirlpoolAddress: PublicKey) {
return AddressUtil.findProgramAddress(
[Buffer.from(PDA_ORACLE_SEED), whirlpoolAddress.toBuffer()],
programId
);
}
}

View File

@ -0,0 +1,231 @@
import { AddressUtil, MathUtil, Percentage } from "@orca-so/common-sdk";
import { Address, BN } from "@project-serum/anchor";
import { u64 } from "@solana/spl-token";
import { PublicKey } from "@solana/web3.js";
import Decimal from "decimal.js";
import { MAX_TICK_ARRAY_CROSSINGS, WhirlpoolRewardInfoData } from "../../types/public";
import { PDAUtil } from "./pda-utils";
import { PriceMath } from "./price-math";
import { TickUtil } from "./tick-utils";
/**
* @category Whirlpool Utils
*/
export class PoolUtil {
private constructor() {}
public static isRewardInitialized(rewardInfo: WhirlpoolRewardInfoData): boolean {
return (
!PublicKey.default.equals(rewardInfo.mint) && !PublicKey.default.equals(rewardInfo.vault)
);
}
public static getFeeRate(feeRate: number): Percentage {
/**
* Smart Contract comment: https://github.com/orca-so/whirlpool/blob/main/programs/whirlpool/src/state/whirlpool.rs#L9-L11
* // Stored as hundredths of a basis point
* // u16::MAX corresponds to ~6.5%
* pub fee_rate: u16,
*/
return Percentage.fromFraction(feeRate, 1e6); // TODO
}
public static getProtocolFeeRate(protocolFeeRate: number): Percentage {
/**
* Smart Contract comment: https://github.com/orca-so/whirlpool/blob/main/programs/whirlpool/src/state/whirlpool.rs#L13-L14
* // Stored as a basis point
* pub protocol_fee_rate: u16,
*/
return Percentage.fromFraction(protocolFeeRate, 1e4); // TODO
}
public static orderMints(mintX: Address, mintY: Address): [Address, Address] {
let mintA, mintB;
if (
Buffer.compare(
AddressUtil.toPubKey(mintX).toBuffer(),
AddressUtil.toPubKey(mintY).toBuffer()
) < 0
) {
mintA = mintX;
mintB = mintY;
} else {
mintA = mintY;
mintB = mintX;
}
return [mintA, mintB];
}
/**
* @category Whirlpool Utils
* @param liquidity
* @param currentPrice
* @param lowerPrice
* @param upperPrice
* @param round_up
* @returns
*/
public static getTokenAmountsFromLiquidity(
liquidity: u64,
currentPrice: u64,
lowerPrice: u64,
upperPrice: u64,
round_up: boolean
): TokenAmounts {
const _liquidity = new Decimal(liquidity.toString());
const _currentPrice = new Decimal(currentPrice.toString());
const _lowerPrice = new Decimal(lowerPrice.toString());
const _upperPrice = new Decimal(upperPrice.toString());
let tokenA, tokenB;
if (currentPrice.lt(lowerPrice)) {
// x = L * (pb - pa) / (pa * pb)
tokenA = MathUtil.toX64_Decimal(_liquidity)
.mul(_upperPrice.sub(_lowerPrice))
.div(_lowerPrice.mul(_upperPrice));
tokenB = new Decimal(0);
} else if (currentPrice.lt(upperPrice)) {
// x = L * (pb - p) / (p * pb)
// y = L * (p - pa)
tokenA = MathUtil.toX64_Decimal(_liquidity)
.mul(_upperPrice.sub(_currentPrice))
.div(_currentPrice.mul(_upperPrice));
tokenB = MathUtil.fromX64_Decimal(_liquidity.mul(_currentPrice.sub(_lowerPrice)));
} else {
// y = L * (pb - pa)
tokenA = new Decimal(0);
tokenB = MathUtil.fromX64_Decimal(_liquidity.mul(_upperPrice.sub(_lowerPrice)));
}
// TODO: round up
if (round_up) {
return {
tokenA: new u64(tokenA.ceil().toString()),
tokenB: new u64(tokenB.ceil().toString()),
};
} else {
return {
tokenA: new u64(tokenA.floor().toString()),
tokenB: new u64(tokenB.floor().toString()),
};
}
}
/**
* Estimate the liquidity amount required to increase/decrease liquidity.
*
* // TODO: At the top end of the price range, tick calcuation is off therefore the results can be off
*
* @category Whirlpool Utils
* @param currTick - Whirlpool's current tick index (aka price)
* @param lowerTick - Position lower tick index
* @param upperTick - Position upper tick index
* @param tokenAmount - The desired amount of tokens to deposit/withdraw
* @returns An estimated amount of liquidity needed to deposit/withdraw the desired amount of tokens.
*/
public static estimateLiquidityFromTokenAmounts(
currTick: number,
lowerTick: number,
upperTick: number,
tokenAmount: TokenAmounts
): BN {
if (upperTick < lowerTick) {
throw new Error("upper tick cannot be lower than the lower tick");
}
const currSqrtPrice = PriceMath.tickIndexToSqrtPriceX64(currTick);
const lowerSqrtPrice = PriceMath.tickIndexToSqrtPriceX64(lowerTick);
const upperSqrtPrice = PriceMath.tickIndexToSqrtPriceX64(upperTick);
if (currTick >= upperTick) {
return estLiquidityForTokenB(upperSqrtPrice, lowerSqrtPrice, tokenAmount.tokenB);
} else if (currTick < lowerTick) {
return estLiquidityForTokenA(lowerSqrtPrice, upperSqrtPrice, tokenAmount.tokenA);
} else {
const estLiquidityAmountA = estLiquidityForTokenA(
currSqrtPrice,
upperSqrtPrice,
tokenAmount.tokenA
);
const estLiquidityAmountB = estLiquidityForTokenB(
currSqrtPrice,
lowerSqrtPrice,
tokenAmount.tokenB
);
return BN.min(estLiquidityAmountA, estLiquidityAmountB);
}
}
/**
* Given the current tick-index, returns the dervied PDA and fetched data
* for the tick-arrays that this swap may traverse across.
*
* TODO: Handle the edge case where the first tick-array may be the previous array of
* expected start-index due to slippage.
*
* @category Whirlpool Utils
* @param tickCurrentIndex - The current tickIndex for the Whirlpool to swap on.
* @param tickSpacing - The tickSpacing for the Whirlpool.
* @param aToB - The direction of the trade.
* @param programId - The Whirlpool programId which the Whirlpool lives on.
* @param whirlpoolAddress - PublicKey of the whirlpool to swap on.
* @returns An array of PublicKey[] for the tickArray accounts that this swap may traverse across.
*/
public static getTickArrayPublicKeysForSwap(
tickCurrentIndex: number,
tickSpacing: number,
aToB: boolean,
programId: PublicKey,
whirlpoolAddress: PublicKey
) {
let offset = 0;
let tickArrayAddresses: PublicKey[] = [];
for (let i = 0; i <= MAX_TICK_ARRAY_CROSSINGS; i++) {
const startIndex = TickUtil.getStartTickIndex(tickCurrentIndex, tickSpacing, offset);
const pda = PDAUtil.getTickArray(programId, whirlpoolAddress, startIndex);
tickArrayAddresses.push(pda.publicKey);
offset = aToB ? offset - 1 : offset + 1;
}
return tickArrayAddresses;
}
}
/**
* @category Whirlpool Utils
*/
export type TokenAmounts = {
tokenA: u64;
tokenB: u64;
};
/**
* @category Whirlpool Utils
*/
export function toTokenAmount(a: number, b: number): TokenAmounts {
return {
tokenA: new u64(a.toString()),
tokenB: new u64(b.toString()),
};
}
// Convert this function based on Delta A = Delta L * (1/sqrt(lower) - 1/sqrt(upper))
function estLiquidityForTokenA(sqrtPrice1: BN, sqrtPrice2: BN, tokenAmount: u64) {
const lowerSqrtPriceX64 = BN.min(sqrtPrice1, sqrtPrice2);
const upperSqrtPriceX64 = BN.max(sqrtPrice1, sqrtPrice2);
const num = MathUtil.fromX64_BN(tokenAmount.mul(upperSqrtPriceX64).mul(lowerSqrtPriceX64));
const dem = upperSqrtPriceX64.sub(lowerSqrtPriceX64);
return num.div(dem);
}
// Convert this function based on Delta B = Delta L * (sqrt_price(upper) - sqrt_price(lower))
function estLiquidityForTokenB(sqrtPrice1: BN, sqrtPrice2: BN, tokenAmount: u64) {
const lowerSqrtPriceX64 = BN.min(sqrtPrice1, sqrtPrice2);
const upperSqrtPriceX64 = BN.max(sqrtPrice1, sqrtPrice2);
const delta = upperSqrtPriceX64.sub(lowerSqrtPriceX64);
return MathUtil.toX64_BN(tokenAmount).div(delta);
}

View File

@ -0,0 +1,279 @@
import { MathUtil } from "@orca-so/common-sdk";
import { BN } from "@project-serum/anchor";
import Decimal from "decimal.js";
import { MAX_TICK_INDEX, MIN_TICK_INDEX, MAX_SQRT_PRICE, MIN_SQRT_PRICE } from "../../types/public";
import { TickUtil } from "./tick-utils";
const BIT_PRECISION = 14;
const LOG_B_2_X32 = "59543866431248";
const LOG_B_P_ERR_MARGIN_LOWER_X64 = "184467440737095516";
const LOG_B_P_ERR_MARGIN_UPPER_X64 = "15793534762490258745";
/**
* A collection of utility functions to convert between price, tickIndex and sqrtPrice.
*
* @category Whirlpool Utils
*/
export class PriceMath {
public static priceToSqrtPriceX64(price: Decimal, decimalsA: number, decimalsB: number): BN {
return MathUtil.toX64(price.mul(Decimal.pow(10, decimalsB - decimalsA)).sqrt());
}
public static sqrtPriceX64ToPrice(
sqrtPriceX64: BN,
decimalsA: number,
decimalsB: number
): Decimal {
return MathUtil.fromX64(sqrtPriceX64)
.pow(2)
.mul(Decimal.pow(10, decimalsA - decimalsB));
}
/**
* @param tickIndex
* @returns
*/
public static tickIndexToSqrtPriceX64(tickIndex: number): BN {
if (tickIndex > MAX_TICK_INDEX || tickIndex < MIN_TICK_INDEX) {
throw new Error("Provided tick index does not fit within supported tick index range.");
}
if (tickIndex > 0) {
return new BN(tickIndexToSqrtPricePositive(tickIndex));
} else {
return new BN(tickIndexToSqrtPriceNegative(tickIndex));
}
}
/**
*
* @param sqrtPriceX64
* @returns
*/
public static sqrtPriceX64ToTickIndex(sqrtPriceX64: BN): number {
if (sqrtPriceX64.gt(new BN(MAX_SQRT_PRICE)) || sqrtPriceX64.lt(new BN(MIN_SQRT_PRICE))) {
throw new Error("Provided sqrtPrice is not within the supported sqrtPrice range.");
}
const msb = sqrtPriceX64.bitLength() - 1;
const adjustedMsb = new BN(msb - 64);
const log2pIntegerX32 = signedShiftLeft(adjustedMsb, 32, 128);
let bit = new BN("8000000000000000", "hex");
let precision = 0;
let log2pFractionX64 = new BN(0);
let r = msb >= 64 ? sqrtPriceX64.shrn(msb - 63) : sqrtPriceX64.shln(63 - msb);
while (bit.gt(new BN(0)) && precision < BIT_PRECISION) {
r = r.mul(r);
let rMoreThanTwo = r.shrn(127);
r = r.shrn(63 + rMoreThanTwo.toNumber());
log2pFractionX64 = log2pFractionX64.add(bit.mul(rMoreThanTwo));
bit = bit.shrn(1);
precision += 1;
}
const log2pFractionX32 = log2pFractionX64.shrn(32);
const log2pX32 = log2pIntegerX32.add(log2pFractionX32);
const logbpX64 = log2pX32.mul(new BN(LOG_B_2_X32));
const tickLow = signedShiftRight(
logbpX64.sub(new BN(LOG_B_P_ERR_MARGIN_LOWER_X64)),
64,
128
).toNumber();
const tickHigh = signedShiftRight(
logbpX64.add(new BN(LOG_B_P_ERR_MARGIN_UPPER_X64)),
64,
128
).toNumber();
if (tickLow == tickHigh) {
return tickLow;
} else {
const derivedTickHighSqrtPriceX64 = PriceMath.tickIndexToSqrtPriceX64(tickHigh);
if (derivedTickHighSqrtPriceX64.lte(sqrtPriceX64)) {
return tickHigh;
} else {
return tickLow;
}
}
}
public static tickIndexToPrice(tickIndex: number, decimalsA: number, decimalsB: number): Decimal {
return PriceMath.sqrtPriceX64ToPrice(
PriceMath.tickIndexToSqrtPriceX64(tickIndex),
decimalsA,
decimalsB
);
}
public static priceToTickIndex(price: Decimal, decimalsA: number, decimalsB: number): number {
return PriceMath.sqrtPriceX64ToTickIndex(
PriceMath.priceToSqrtPriceX64(price, decimalsA, decimalsB)
);
}
public static priceToInitializableTickIndex(
price: Decimal,
decimalsA: number,
decimalsB: number,
tickSpacing: number
): number {
return TickUtil.getInitializableTickIndex(
PriceMath.priceToTickIndex(price, decimalsA, decimalsB),
tickSpacing
);
}
}
// Private Functions
function tickIndexToSqrtPricePositive(tick: number) {
let ratio: BN;
if ((tick & 1) != 0) {
ratio = new BN("79232123823359799118286999567");
} else {
ratio = new BN("79228162514264337593543950336");
}
if ((tick & 2) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("79236085330515764027303304731")), 96, 256);
}
if ((tick & 4) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("79244008939048815603706035061")), 96, 256);
}
if ((tick & 8) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("79259858533276714757314932305")), 96, 256);
}
if ((tick & 16) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("79291567232598584799939703904")), 96, 256);
}
if ((tick & 32) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("79355022692464371645785046466")), 96, 256);
}
if ((tick & 64) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("79482085999252804386437311141")), 96, 256);
}
if ((tick & 128) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("79736823300114093921829183326")), 96, 256);
}
if ((tick & 256) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("80248749790819932309965073892")), 96, 256);
}
if ((tick & 512) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("81282483887344747381513967011")), 96, 256);
}
if ((tick & 1024) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("83390072131320151908154831281")), 96, 256);
}
if ((tick & 2048) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("87770609709833776024991924138")), 96, 256);
}
if ((tick & 4096) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("97234110755111693312479820773")), 96, 256);
}
if ((tick & 8192) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("119332217159966728226237229890")), 96, 256);
}
if ((tick & 16384) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("179736315981702064433883588727")), 96, 256);
}
if ((tick & 32768) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("407748233172238350107850275304")), 96, 256);
}
if ((tick & 65536) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("2098478828474011932436660412517")), 96, 256);
}
if ((tick & 131072) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("55581415166113811149459800483533")), 96, 256);
}
if ((tick & 262144) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("38992368544603139932233054999993551")), 96, 256);
}
return signedShiftRight(ratio, 32, 256);
}
function tickIndexToSqrtPriceNegative(tickIndex: number) {
let tick = Math.abs(tickIndex);
let ratio: BN;
if ((tick & 1) != 0) {
ratio = new BN("18445821805675392311");
} else {
ratio = new BN("18446744073709551616");
}
if ((tick & 2) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("18444899583751176498")), 64, 256);
}
if ((tick & 4) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("18443055278223354162")), 64, 256);
}
if ((tick & 8) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("18439367220385604838")), 64, 256);
}
if ((tick & 16) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("18431993317065449817")), 64, 256);
}
if ((tick & 32) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("18417254355718160513")), 64, 256);
}
if ((tick & 64) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("18387811781193591352")), 64, 256);
}
if ((tick & 128) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("18329067761203520168")), 64, 256);
}
if ((tick & 256) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("18212142134806087854")), 64, 256);
}
if ((tick & 512) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("17980523815641551639")), 64, 256);
}
if ((tick & 1024) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("17526086738831147013")), 64, 256);
}
if ((tick & 2048) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("16651378430235024244")), 64, 256);
}
if ((tick & 4096) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("15030750278693429944")), 64, 256);
}
if ((tick & 8192) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("12247334978882834399")), 64, 256);
}
if ((tick & 16384) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("8131365268884726200")), 64, 256);
}
if ((tick & 32768) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("3584323654723342297")), 64, 256);
}
if ((tick & 65536) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("696457651847595233")), 64, 256);
}
if ((tick & 131072) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("26294789957452057")), 64, 256);
}
if ((tick & 262144) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("37481735321082")), 64, 256);
}
return ratio;
}
function signedShiftLeft(n0: BN, shiftBy: number, bitWidth: number) {
let twosN0 = n0.toTwos(bitWidth).shln(shiftBy);
twosN0.imaskn(bitWidth + 1);
return twosN0.fromTwos(bitWidth);
}
function signedShiftRight(n0: BN, shiftBy: number, bitWidth: number) {
let twoN0 = n0.toTwos(bitWidth).shrn(shiftBy);
twoN0.imaskn(bitWidth - shiftBy + 1);
return twoN0.fromTwos(bitWidth - shiftBy);
}

View File

@ -1,229 +1,203 @@
import { BN } from "@project-serum/anchor";
import invariant from "tiny-invariant";
import { Address, BN } from "@project-serum/anchor";
import { PublicKey } from "@solana/web3.js";
import {
MAX_TICK_INDEX,
MIN_TICK_INDEX,
TickArrayData,
TickData,
TICK_ARRAY_SIZE,
} from "../../types/public";
import { PDAUtil } from "./pda-utils";
export const MAX_TICK_INDEX = 443636;
export const MIN_TICK_INDEX = -443636;
export const TICK_ARRAY_SIZE = 88;
export const MAX_SQRT_PRICE = "79226673515401279992447579055";
export const MIN_SQRT_PRICE = "4295048016";
const BIT_PRECISION = 14;
const LOG_B_2_X32 = "59543866431248";
const LOG_B_P_ERR_MARGIN_LOWER_X64 = "184467440737095516";
const LOG_B_P_ERR_MARGIN_UPPER_X64 = "15793534762490258745";
export function tickIndexToSqrtPriceX64(tickIndex: number): BN {
if (tickIndex > MAX_TICK_INDEX || tickIndex < MIN_TICK_INDEX) {
throw new Error("Provided tick index does not fit within supported tick index range.");
}
if (tickIndex > 0) {
return new BN(tickIndexToSqrtPricePositive(tickIndex));
} else {
return new BN(tickIndexToSqrtPriceNegative(tickIndex));
}
enum TickSearchDirection {
Left,
Right,
}
export function sqrtPriceX64ToTickIndex(sqrtPriceX64: BN): number {
if (sqrtPriceX64.gt(new BN(MAX_SQRT_PRICE)) || sqrtPriceX64.lt(new BN(MIN_SQRT_PRICE))) {
throw new Error("Provided sqrtPrice is not within the supported sqrtPrice range.");
/**
* A collection of utility functions when interacting with Ticks.
* @category Whirlpool Utils
*/
export class TickUtil {
private constructor() {}
/**
* Get the startIndex of the tick array containing tickIndex.
*
* @param tickIndex
* @param tickSpacing
* @param offset can be used to get neighboring tick array startIndex.
* @returns
*/
public static getStartTickIndex(tickIndex: number, tickSpacing: number, offset = 0): number {
const realIndex = Math.floor(tickIndex / tickSpacing / TICK_ARRAY_SIZE);
const startTickIndex = (realIndex + offset) * tickSpacing * TICK_ARRAY_SIZE;
const ticksInArray = TICK_ARRAY_SIZE * tickSpacing;
const minTickIndex = MIN_TICK_INDEX - ((MIN_TICK_INDEX % ticksInArray) + ticksInArray);
invariant(startTickIndex >= minTickIndex, "startTickIndex is too small");
invariant(startTickIndex <= MAX_TICK_INDEX, "startTickIndex is too large");
return startTickIndex;
}
const msb = sqrtPriceX64.bitLength() - 1;
const adjustedMsb = new BN(msb - 64);
const log2pIntegerX32 = signedShiftLeft(adjustedMsb, 32, 128);
let bit = new BN("8000000000000000", "hex");
let precision = 0;
let log2pFractionX64 = new BN(0);
let r = msb >= 64 ? sqrtPriceX64.shrn(msb - 63) : sqrtPriceX64.shln(63 - msb);
while (bit.gt(new BN(0)) && precision < BIT_PRECISION) {
r = r.mul(r);
let rMoreThanTwo = r.shrn(127);
r = r.shrn(63 + rMoreThanTwo.toNumber());
log2pFractionX64 = log2pFractionX64.add(bit.mul(rMoreThanTwo));
bit = bit.shrn(1);
precision += 1;
/**
* Get the nearest (rounding down) valid tick index from the tickIndex.
* A valid tick index is a point on the tick spacing grid line.
*/
public static getInitializableTickIndex(tickIndex: number, tickSpacing: number): number {
return tickIndex - (tickIndex % tickSpacing);
}
const log2pFractionX32 = log2pFractionX64.shrn(32);
public static getNextInitializableTickIndex(tickIndex: number, tickSpacing: number) {
return TickUtil.getInitializableTickIndex(tickIndex, tickSpacing) + tickSpacing;
}
const log2pX32 = log2pIntegerX32.add(log2pFractionX32);
const logbpX64 = log2pX32.mul(new BN(LOG_B_2_X32));
public static getPrevInitializableTickIndex(tickIndex: number, tickSpacing: number) {
return TickUtil.getInitializableTickIndex(tickIndex, tickSpacing) - tickSpacing;
}
const tickLow = signedShiftRight(
logbpX64.sub(new BN(LOG_B_P_ERR_MARGIN_LOWER_X64)),
64,
128
).toNumber();
const tickHigh = signedShiftRight(
logbpX64.add(new BN(LOG_B_P_ERR_MARGIN_UPPER_X64)),
64,
128
).toNumber();
/**
* Get the previous initialized tick index within the same tick array.
*
* @param account
* @param currentTickIndex
* @param tickSpacing
* @returns
*/
public static findPreviousInitializedTickIndex(
account: TickArrayData,
currentTickIndex: number,
tickSpacing: number
): number | null {
return TickUtil.findInitializedTick(
account,
currentTickIndex,
tickSpacing,
TickSearchDirection.Left
);
}
if (tickLow == tickHigh) {
return tickLow;
} else {
const derivedTickHighSqrtPriceX64 = tickIndexToSqrtPriceX64(tickHigh);
if (derivedTickHighSqrtPriceX64.lte(sqrtPriceX64)) {
return tickHigh;
} else {
return tickLow;
/**
* Get the next initialized tick index within the same tick array.
* @param account
* @param currentTickIndex
* @param tickSpacing
* @returns
*/
public static findNextInitializedTickIndex(
account: TickArrayData,
currentTickIndex: number,
tickSpacing: number
): number | null {
return TickUtil.findInitializedTick(
account,
currentTickIndex,
tickSpacing,
TickSearchDirection.Right
);
}
private static findInitializedTick(
account: TickArrayData,
currentTickIndex: number,
tickSpacing: number,
searchDirection: TickSearchDirection
): number | null {
const currentTickArrayIndex = tickIndexToInnerIndex(
account.startTickIndex,
currentTickIndex,
tickSpacing
);
const increment = searchDirection === TickSearchDirection.Right ? 1 : -1;
let stepInitializedTickArrayIndex =
searchDirection === TickSearchDirection.Right
? currentTickArrayIndex + increment
: currentTickArrayIndex;
while (
stepInitializedTickArrayIndex >= 0 &&
stepInitializedTickArrayIndex < account.ticks.length
) {
if (account.ticks[stepInitializedTickArrayIndex]?.initialized) {
return innerIndexToTickIndex(
account.startTickIndex,
stepInitializedTickArrayIndex,
tickSpacing
);
}
stepInitializedTickArrayIndex += increment;
}
return null;
}
public static checkTickInBounds(tick: number) {
return tick <= MAX_TICK_INDEX && tick >= MIN_TICK_INDEX;
}
public static isTickInitializable(tick: number, tickSpacing: number) {
return tick % tickSpacing === 0;
}
}
export function getStartTickIndex(tickIndex: number, tickSpacing: number): number {
const ticksInArray = tickSpacing * TICK_ARRAY_SIZE;
return Math.floor(tickIndex / ticksInArray) * ticksInArray;
export class TickArrayUtil {
/**
* Get the tick from tickArray with a global tickIndex.
*/
public static getTickFromArray(
tickArray: TickArrayData,
tickIndex: number,
tickSpacing: number
): TickData {
const realIndex = tickIndexToInnerIndex(tickArray.startTickIndex, tickIndex, tickSpacing);
const tick = tickArray.ticks[realIndex];
invariant(
!!tick,
`tick realIndex out of range - start - ${tickArray.startTickIndex} index - ${tickIndex}, realIndex - ${realIndex}`
);
return tick;
}
/**
*
* @param tickLowerIndex
* @param tickUpperIndex
* @param tickSpacing
* @param whirlpool
* @param programId
* @returns
*/
public static getAdjacentTickArrays(
tickLowerIndex: number,
tickUpperIndex: number,
tickSpacing: number,
whirlpool: PublicKey,
programId: PublicKey
): [PublicKey, PublicKey] {
return [
PDAUtil.getTickArrayFromTickIndex(tickLowerIndex, tickSpacing, whirlpool, programId)
.publicKey,
PDAUtil.getTickArrayFromTickIndex(tickUpperIndex, tickSpacing, whirlpool, programId)
.publicKey,
];
}
}
function tickIndexToSqrtPricePositive(tick: number) {
let ratio: BN;
if ((tick & 1) != 0) {
ratio = new BN("79232123823359799118286999567");
} else {
ratio = new BN("79228162514264337593543950336");
}
if ((tick & 2) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("79236085330515764027303304731")), 96, 256);
}
if ((tick & 4) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("79244008939048815603706035061")), 96, 256);
}
if ((tick & 8) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("79259858533276714757314932305")), 96, 256);
}
if ((tick & 16) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("79291567232598584799939703904")), 96, 256);
}
if ((tick & 32) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("79355022692464371645785046466")), 96, 256);
}
if ((tick & 64) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("79482085999252804386437311141")), 96, 256);
}
if ((tick & 128) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("79736823300114093921829183326")), 96, 256);
}
if ((tick & 256) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("80248749790819932309965073892")), 96, 256);
}
if ((tick & 512) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("81282483887344747381513967011")), 96, 256);
}
if ((tick & 1024) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("83390072131320151908154831281")), 96, 256);
}
if ((tick & 2048) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("87770609709833776024991924138")), 96, 256);
}
if ((tick & 4096) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("97234110755111693312479820773")), 96, 256);
}
if ((tick & 8192) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("119332217159966728226237229890")), 96, 256);
}
if ((tick & 16384) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("179736315981702064433883588727")), 96, 256);
}
if ((tick & 32768) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("407748233172238350107850275304")), 96, 256);
}
if ((tick & 65536) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("2098478828474011932436660412517")), 96, 256);
}
if ((tick & 131072) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("55581415166113811149459800483533")), 96, 256);
}
if ((tick & 262144) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("38992368544603139932233054999993551")), 96, 256);
}
return signedShiftRight(ratio, 32, 256);
function tickIndexToInnerIndex(
startTickIndex: number,
tickIndex: number,
tickSpacing: number
): number {
return Math.floor((tickIndex - startTickIndex) / tickSpacing);
}
function tickIndexToSqrtPriceNegative(tickIndex: number) {
let tick = Math.abs(tickIndex);
let ratio: BN;
if ((tick & 1) != 0) {
ratio = new BN("18445821805675392311");
} else {
ratio = new BN("18446744073709551616");
}
if ((tick & 2) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("18444899583751176498")), 64, 256);
}
if ((tick & 4) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("18443055278223354162")), 64, 256);
}
if ((tick & 8) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("18439367220385604838")), 64, 256);
}
if ((tick & 16) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("18431993317065449817")), 64, 256);
}
if ((tick & 32) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("18417254355718160513")), 64, 256);
}
if ((tick & 64) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("18387811781193591352")), 64, 256);
}
if ((tick & 128) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("18329067761203520168")), 64, 256);
}
if ((tick & 256) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("18212142134806087854")), 64, 256);
}
if ((tick & 512) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("17980523815641551639")), 64, 256);
}
if ((tick & 1024) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("17526086738831147013")), 64, 256);
}
if ((tick & 2048) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("16651378430235024244")), 64, 256);
}
if ((tick & 4096) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("15030750278693429944")), 64, 256);
}
if ((tick & 8192) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("12247334978882834399")), 64, 256);
}
if ((tick & 16384) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("8131365268884726200")), 64, 256);
}
if ((tick & 32768) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("3584323654723342297")), 64, 256);
}
if ((tick & 65536) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("696457651847595233")), 64, 256);
}
if ((tick & 131072) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("26294789957452057")), 64, 256);
}
if ((tick & 262144) != 0) {
ratio = signedShiftRight(ratio.mul(new BN("37481735321082")), 64, 256);
}
return ratio;
}
function signedShiftLeft(n0: BN, shiftBy: number, bitWidth: number) {
let twosN0 = n0.toTwos(bitWidth).shln(shiftBy);
twosN0.imaskn(bitWidth + 1);
return twosN0.fromTwos(bitWidth);
}
function signedShiftRight(n0: BN, shiftBy: number, bitWidth: number) {
let twoN0 = n0.toTwos(bitWidth).shrn(shiftBy);
twoN0.imaskn(bitWidth - shiftBy + 1);
return twoN0.fromTwos(bitWidth - shiftBy);
function innerIndexToTickIndex(
startTickIndex: number,
tickArrayIndex: number,
tickSpacing: number
): number {
return startTickIndex + tickArrayIndex * tickSpacing;
}

View File

@ -0,0 +1,28 @@
import { MathUtil } from "@orca-so/common-sdk";
import BN from "bn.js";
export function getLowerSqrtPriceFromTokenA(amount: BN, liquidity: BN, sqrtPriceX64: BN): BN {
const numerator = liquidity.mul(sqrtPriceX64).shln(64);
const denominator = liquidity.shln(64).add(amount.mul(sqrtPriceX64));
// always round up
return MathUtil.divRoundUp(numerator, denominator);
}
export function getUpperSqrtPriceFromTokenA(amount: BN, liquidity: BN, sqrtPriceX64: BN): BN {
const numerator = liquidity.mul(sqrtPriceX64).shln(64);
const denominator = liquidity.shln(64).sub(amount.mul(sqrtPriceX64));
// always round up
return MathUtil.divRoundUp(numerator, denominator);
}
export function getLowerSqrtPriceFromTokenB(amount: BN, liquidity: BN, sqrtPriceX64: BN): BN {
// always round down
return sqrtPriceX64.sub(MathUtil.divRoundUp(amount.shln(64), liquidity));
}
export function getUpperSqrtPriceFromTokenB(amount: BN, liquidity: BN, sqrtPriceX64: BN): BN {
// always round down (rounding up a negative number)
return sqrtPriceX64.add(amount.shln(64).div(liquidity));
}

View File

@ -1,107 +0,0 @@
import { Provider } from "@project-serum/anchor";
import { Transaction, Signer, TransactionInstruction } from "@solana/web3.js";
export const EMPTY_INSTRUCTION: Instruction = {
instructions: [],
cleanupInstructions: [],
signers: [],
};
export type Instruction = {
instructions: TransactionInstruction[];
cleanupInstructions: TransactionInstruction[];
signers: Signer[];
};
export type TransactionPayload = {
transaction: Transaction;
signers: Signer[];
};
export class TransactionBuilder {
private provider: Provider;
private instructions: Instruction[];
private signers: Signer[];
constructor(provider: Provider) {
this.provider = provider;
this.instructions = [];
this.signers = [];
}
addInstruction(instruction: Instruction): TransactionBuilder {
this.instructions.push(instruction);
return this;
}
addSigner(signer: Signer): TransactionBuilder {
this.signers.push(signer);
return this;
}
isEmpty(): boolean {
return this.instructions.length == 0;
}
/**
* Compresses all instructions & signers in this builder
* into one single instruction
* @param compressPost Compress all post instructions into the instructions field
* @returns Instruction object containing all
*/
compressIx(compressPost: boolean): Instruction {
let instructions: TransactionInstruction[] = [];
let cleanupInstructions: TransactionInstruction[] = [];
let signers: Signer[] = [];
this.instructions.forEach((curr) => {
instructions = instructions.concat(curr.instructions);
// Cleanup instructions should execute in reverse order
cleanupInstructions = curr.cleanupInstructions.concat(cleanupInstructions);
signers = signers.concat(curr.signers);
});
if (compressPost) {
instructions = instructions.concat(cleanupInstructions);
cleanupInstructions = [];
}
return {
instructions: [...instructions],
cleanupInstructions: [...cleanupInstructions],
signers,
};
}
/**
* Constructs a transaction payload with the gathered instructions
* @returns a TransactionPayload object that can be excuted or agregated into other transactions
*/
async build(): Promise<TransactionPayload> {
const recentBlockHash = (await this.provider.connection.getRecentBlockhash("singleGossip"))
.blockhash;
const transaction = new Transaction({
recentBlockhash: recentBlockHash,
feePayer: this.provider.wallet.publicKey,
});
const ix = this.compressIx(true);
transaction.add(...ix.instructions);
transaction.feePayer = this.provider.wallet.publicKey;
return {
transaction: transaction,
signers: ix.signers.concat(this.signers),
};
}
/**
* Constructs a transaction payload with the gathered instructions, sign it with the provider and send it out
* @returns the txId of the transaction
*/
async buildAndExecute(): Promise<string> {
const tx = await this.build();
return this.provider.send(tx.transaction, tx.signers, { commitment: "confirmed" });
}
}

211
sdk/src/whirlpool-client.ts Normal file
View File

@ -0,0 +1,211 @@
import { Percentage, TransactionBuilder } from "@orca-so/common-sdk";
import { Address } from "@project-serum/anchor";
import { WhirlpoolContext } from "./context";
import { AccountFetcher } from "./network/public";
import {
WhirlpoolData,
PositionData,
IncreaseLiquidityInput,
DecreaseLiquidityInput,
} from "./types/public";
import { TokenInfo } from "./types/public/client-types";
import { PublicKey } from "@solana/web3.js";
import { SwapQuote } from "./quotes/public";
import { WhirlpoolClientImpl } from "./impl/whirlpool-client-impl";
export interface WhirlpoolClient {
/**
* Get a Whirlpool object to interact with the Whirlpool account at the given address.
* @param poolAddress the address of the Whirlpool account
* @return a Whirlpool object to interact with
*/
getPool: (poolAddress: Address) => Promise<Whirlpool>;
/**
* Get a Position object to interact with the Position account at the given address.
* @param positionAddress the address of the Position account
* @return a Position object to interact with
*/
getPosition: (positionAddress: Address) => Promise<Position>;
}
/**
* Construct a WhirlpoolClient instance to help interact with Whirlpools accounts with.
*
* @param ctx - WhirlpoolContext object
* @param fetcher - AccountFetcher instance to help fetch data with.
* @returns a WhirlpoolClient instance to help with interacting with Whirlpools accounts.
*/
export function buildWhirlpoolClient(ctx: WhirlpoolContext, fetcher: AccountFetcher) {
return new WhirlpoolClientImpl(ctx, fetcher);
}
/**
* Helper class to interact with a Whirlpool account and build complex transactions.
*/
export interface Whirlpool {
/**
* Return the most recently fetched Whirlpool account data.
* @return most recently fetched WhirlpoolData for this address.
*/
getData: () => WhirlpoolData;
/**
* Fetch and return the most recently fetched Whirlpool account data.
* @return the most up to date WhirlpoolData for this address.
*/
refreshData: () => Promise<WhirlpoolData>;
/**
* Get the TokenInfo for token A of this pool.
* @return TokenInfo for token A
*/
getTokenAInfo: () => TokenInfo;
/**
* Get the TokenInfo for token B of this pool.
* @return TokenInfo for token B
*/
getTokenBInfo: () => TokenInfo;
// TODO: add functionality to check whether tick arrays exists
/**
* Initialize a set of tick-arrays to support the provided ticks.
*
* This function does not ensure the provided tick index are within range and not initialized.
*
* If `funder` is provided, the funder wallet has to sign this transaction.
*
* @param ticks - A group of ticks that define the desired tick-arrays to initialize
* @param funder - optional - the wallet that will fund the cost needed to initialize the position. If null, the WhirlpoolContext wallet is used.
* @return a transaction that will initialize the defined tick-arrays if executed.
*/
initTickArrayForTicks: (ticks: number[], funder?: Address) => TransactionBuilder;
/**
* Open and fund a position on this Whirlpool.
*
* User has to ensure the TickArray for tickLower and tickUpper has been initialized prior to calling this function.
*
* If `funder` is provided, the funder wallet has to sign this transaction.
*
* @param tickLower - the tick index for the lower bound of this position
* @param tickUpper - the tick index for the upper bound of this position
* @param liquidityInput - an InputLiquidityInput type to define the desired liquidity amount to deposit
* @param sourceWallet - optional - the wallet to withdraw tokens to deposit into the position. If null, the WhirlpoolContext wallet is used.
* @param positionWallet - optional - the wallet to that houses the position token. If null, the WhirlpoolContext wallet is used.
* @param funder - optional - the wallet that will fund the cost needed to initialize the position. If null, the WhirlpoolContext wallet is used.
* @return `positionMint` - the position to be created. `tx` - The transaction containing the instructions to perform the operation on chain.
*/
openPosition: (
tickLower: number,
tickUpper: number,
liquidityInput: IncreaseLiquidityInput,
sourceWallet?: Address,
positionWallet?: Address,
funder?: Address
) => Promise<{ positionMint: PublicKey; tx: TransactionBuilder }>;
/**
* Open and fund a position with meta-data on this Whirlpool.
*
* User has to ensure the TickArray for tickLower and tickUpper has been initialized prior to calling this function.
*
* If `sourceWallet`, `positionWallet` or `funder` is provided, the wallet owners have to sign this transaction.
*
* @param tickLower - the tick index for the lower bound of this position
* @param tickUpper - the tick index for the upper bound of this position
* @param liquidityInput - input that defines the desired liquidity amount and maximum tokens willing to be to deposited.
* @param sourceWallet - optional - the wallet to withdraw tokens to deposit into the position. If null, the WhirlpoolContext wallet is used.
* @param positionWallet - optional - the wallet to that houses the position token. If null, the WhirlpoolContext wallet is used.
* @param funder - optional - the wallet that will fund the cost needed to initialize the position. If null, the WhirlpoolContext wallet is used.
* @return `positionMint` - the position to be created. `tx` - The transaction containing the instructions to perform the operation on chain.
*/
openPositionWithMetadata: (
tickLower: number,
tickUpper: number,
liquidityInput: IncreaseLiquidityInput,
sourceWallet?: Address,
positionWallet?: Address,
funder?: Address
) => Promise<{ positionMint: PublicKey; tx: TransactionBuilder }>;
/**
* Withdraw all tokens from a position, close the account and burn the position token.
*
* Users have to collect all fees and rewards from this position prior to closing the account.
*
* If `positionWallet` is provided, the wallet owner has to sign this transaction.
*
* @param positionAddress - The address of the position account.
* @param slippageTolerance - The amount of slippage the caller is willing to accept when withdrawing liquidity.
* @param destinationWallet - optional - The wallet that the tokens withdrawn will be sent to. If null, the WhirlpoolContext wallet is used.
* @param positionWallet - optional - The wallet that houses the position token that corresponds to this position address. If null, the WhirlpoolContext wallet is used.
*/
closePosition: (
positionAddress: Address,
slippageTolerance: Percentage,
destinationWallet?: Address,
positionWallet?: Address
) => Promise<TransactionBuilder>;
/**
* Perform a swap between tokenA and tokenB on this pool.
*
* @param quote - A quote on the desired tokenIn and tokenOut for this swap. Use @link {swapQuote} to generate this object.
* @param wallet - The wallet that tokens will be withdrawn and deposit into. If null, the WhirlpoolContext wallet is used.
* @return a transaction that will perform the swap once executed.
*/
swap: (quote: SwapQuote, wallet?: PublicKey) => Promise<TransactionBuilder>;
}
/**
* Helper class to interact with a Position account and build complex transactions.
*/
export interface Position {
/**
* Return the most recently fetched Position account data.
* @return most recently fetched PositionData for this address.
*/
getData: () => PositionData;
/**
* Fetch and return the most recently fetched Position account data.
* @return the most up to date PositionData for this address.
*/
refreshData: () => Promise<PositionData>;
/**
* Deposit additional tokens into this postiion.
*
* If `sourceWallet`, `positionWallet` is provided, the wallet owners have to sign this transaction.
*
* @param liquidityInput - input that defines the desired liquidity amount and maximum tokens willing to be to deposited.
* @param sourceWallet - optional - the wallet to withdraw tokens to deposit into the position. If null, the WhirlpoolContext wallet is used.
* @param positionWallet - optional - the wallet to that houses the position token. If null, the WhirlpoolContext wallet is used.
* @return the transaction that will deposit the tokens into the position when executed.
*/
increaseLiquidity: (
liquidityInput: IncreaseLiquidityInput,
sourceWallet?: Address,
positionWallet?: Address
) => Promise<TransactionBuilder>;
/**
* Withdraw liquidity from this position.
*
* If `positionWallet` is provided, the wallet owners have to sign this transaction.
*
* @param liquidityInput - input that defines the desired liquidity amount and minimum tokens willing to be to withdrawn from the position.
* @param sourceWallet - optional - the wallet to deposit tokens into when withdrawing from the position. If null, the WhirlpoolContext wallet is used.
* @param positionWallet - optional - the wallet to that houses the position token. If null, the WhirlpoolContext wallet is used.
* @return the transaction that will deposit the tokens into the position when executed.
*/
decreaseLiquidity: (
liquidityInput: DecreaseLiquidityInput,
sourceWallet?: Address,
positionWallet?: Address
) => Promise<TransactionBuilder>;
// TODO: Implement Collect fees
}

View File

@ -1,55 +0,0 @@
import * as assert from "assert";
import * as anchor from "@project-serum/anchor";
import { WhirlpoolContext } from "../src/context";
import { WhirlpoolClient } from "../src/client";
import { InitConfigParams } from "../src/types/public/ix-types";
import { generateDefaultConfigParams } from "./utils/test-builders";
import { ONE_SOL, systemTransferTx } from "./utils";
describe("initialize_config", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
let initializedConfigInfo: InitConfigParams;
it("successfully init a WhirlpoolsConfig account", async () => {
const { configInitInfo } = generateDefaultConfigParams(context);
await client.initConfigTx(configInitInfo).buildAndExecute();
const configAccount = await client.getConfig(configInitInfo.whirlpoolConfigKeypair.publicKey);
assert.ok(
configAccount.collectProtocolFeesAuthority.equals(configInitInfo.collectProtocolFeesAuthority)
);
assert.ok(configAccount.feeAuthority.equals(configInitInfo.feeAuthority));
assert.ok(
configAccount.rewardEmissionsSuperAuthority.equals(
configInitInfo.rewardEmissionsSuperAuthority
)
);
assert.equal(configAccount.defaultProtocolFeeRate, configInitInfo.defaultProtocolFeeRate);
initializedConfigInfo = configInitInfo;
});
it("fail on passing in already initialized whirlpool account", async () => {
let infoWithDupeConfigKey = {
...generateDefaultConfigParams(context).configInitInfo,
whirlpoolConfigKeypair: initializedConfigInfo.whirlpoolConfigKeypair,
};
await assert.rejects(client.initConfigTx(infoWithDupeConfigKey).buildAndExecute(), /0x0/);
});
it("succeeds when funder is different than account paying for transaction fee", async () => {
const funderKeypair = anchor.web3.Keypair.generate();
await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute();
const { configInitInfo } = generateDefaultConfigParams(context, funderKeypair.publicKey);
await client.initConfigTx(configInitInfo).addSigner(funderKeypair).buildAndExecute();
});
});

View File

@ -1,8 +1,7 @@
import * as assert from "assert";
import * as anchor from "@project-serum/anchor";
import { WhirlpoolContext } from "../src/context";
import { WhirlpoolClient } from "../src/client";
import { initTestPool, initTestPoolWithLiquidity, openPosition } from "./utils/init-utils";
import { WhirlpoolContext } from "../../src/context";
import { initTestPool, initTestPoolWithLiquidity, openPosition } from "../utils/init-utils";
import {
approveToken,
createAndMintToTokenAccount,
@ -11,40 +10,39 @@ import {
TickSpacing,
transfer,
ZERO_BN,
} from "./utils";
import { WhirlpoolTestFixture } from "./utils/fixture";
} from "../utils";
import { WhirlpoolTestFixture } from "../utils/fixture";
import { WhirlpoolIx } from "../../src";
import { toTx } from "../../src/utils/instructions-util";
describe("close_position", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
it("successfully closes an open position", async () => {
const { poolInitInfo } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo } = await initTestPool(ctx, TickSpacing.Standard);
const { params } = await openPosition(client, poolInitInfo.whirlpoolPda.publicKey, 0, 128);
const { params } = await openPosition(ctx, poolInitInfo.whirlpoolPda.publicKey, 0, 128);
const receiverKeypair = anchor.web3.Keypair.generate();
await client
.closePositionTx({
await toTx(
ctx,
WhirlpoolIx.closePositionIx(ctx.program, {
positionAuthority: provider.wallet.publicKey,
receiver: receiverKeypair.publicKey,
position: params.positionPda.publicKey,
positionMint: params.positionMintAddress,
positionTokenAccount: params.positionTokenAccountAddress,
positionTokenAccount: params.positionTokenAccount,
})
.buildAndExecute();
).buildAndExecute();
const supplyResponse = await provider.connection.getTokenSupply(params.positionMintAddress);
assert.equal(supplyResponse.value.uiAmount, 0);
assert.equal(await provider.connection.getAccountInfo(params.positionPda.publicKey), undefined);
assert.equal(
await provider.connection.getAccountInfo(params.positionTokenAccountAddress),
undefined
);
assert.equal(await provider.connection.getAccountInfo(params.positionTokenAccount), undefined);
const receiverAccount = await provider.connection.getAccountInfo(receiverKeypair.publicKey);
const lamports = receiverAccount?.lamports;
@ -52,82 +50,74 @@ describe("close_position", () => {
});
it("succeeds if the position is delegated", async () => {
const { poolInitInfo } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo } = await initTestPool(ctx, TickSpacing.Standard);
const owner = anchor.web3.Keypair.generate();
const delegate = anchor.web3.Keypair.generate();
const { params } = await openPosition(
client,
ctx,
poolInitInfo.whirlpoolPda.publicKey,
0,
128,
owner.publicKey
);
await approveToken(
client.context.provider,
params.positionTokenAccountAddress,
delegate.publicKey,
1,
owner
);
await approveToken(ctx.provider, params.positionTokenAccount, delegate.publicKey, 1, owner);
await setAuthority(
client.context.provider,
params.positionTokenAccountAddress,
ctx.provider,
params.positionTokenAccount,
delegate.publicKey,
"CloseAccount",
owner
);
await client
.closePositionTx({
await toTx(
ctx,
WhirlpoolIx.closePositionIx(ctx.program, {
positionAuthority: delegate.publicKey,
receiver: owner.publicKey,
position: params.positionPda.publicKey,
positionMint: params.positionMintAddress,
positionTokenAccount: params.positionTokenAccountAddress,
positionTokenAccount: params.positionTokenAccount,
})
)
.addSigner(delegate)
.buildAndExecute();
});
it("succeeds with the owner's signature even if the token is delegated", async () => {
const { poolInitInfo } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo } = await initTestPool(ctx, TickSpacing.Standard);
const owner = anchor.web3.Keypair.generate();
const delegate = anchor.web3.Keypair.generate();
const { params } = await openPosition(
client,
ctx,
poolInitInfo.whirlpoolPda.publicKey,
0,
128,
owner.publicKey
);
await approveToken(
client.context.provider,
params.positionTokenAccountAddress,
delegate.publicKey,
1,
owner
);
await approveToken(ctx.provider, params.positionTokenAccount, delegate.publicKey, 1, owner);
await client
.closePositionTx({
await toTx(
ctx,
WhirlpoolIx.closePositionIx(ctx.program, {
positionAuthority: owner.publicKey,
receiver: owner.publicKey,
position: params.positionPda.publicKey,
positionMint: params.positionMintAddress,
positionTokenAccount: params.positionTokenAccountAddress,
positionTokenAccount: params.positionTokenAccount,
})
)
.addSigner(owner)
.buildAndExecute();
});
it("succeeds with position token that was transferred to new owner", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
positions: [{ tickLowerIndex: 0, tickUpperIndex: 128, liquidityAmount: ZERO_BN }],
});
@ -142,44 +132,47 @@ describe("close_position", () => {
await transfer(provider, position.tokenAccount, newOwnerPositionTokenAccount, 1);
await client
.closePositionTx({
await toTx(
ctx,
WhirlpoolIx.closePositionIx(ctx.program, {
positionAuthority: newOwner.publicKey,
receiver: newOwner.publicKey,
position: position.publicKey,
positionMint: position.mintKeypair.publicKey,
positionTokenAccount: newOwnerPositionTokenAccount,
})
)
.addSigner(newOwner)
.buildAndExecute();
});
it("fails to close a position with liquidity", async () => {
const { positionInfo } = await initTestPoolWithLiquidity(client);
const { positionInfo } = await initTestPoolWithLiquidity(ctx);
const receiverKeypair = anchor.web3.Keypair.generate();
await assert.rejects(
client
.closePositionTx({
toTx(
ctx,
WhirlpoolIx.closePositionIx(ctx.program, {
positionAuthority: provider.wallet.publicKey,
receiver: receiverKeypair.publicKey,
position: positionInfo.positionPda.publicKey,
positionMint: positionInfo.positionMintAddress,
positionTokenAccount: positionInfo.positionTokenAccountAddress,
positionTokenAccount: positionInfo.positionTokenAccount,
})
.buildAndExecute(),
).buildAndExecute(),
/0x1775/ // ClosePositionNotEmpty
);
});
it("fails if owner is not signer", async () => {
const { poolInitInfo } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo } = await initTestPool(ctx, TickSpacing.Standard);
const owner = anchor.web3.Keypair.generate();
const { params } = await openPosition(
client,
ctx,
poolInitInfo.whirlpoolPda.publicKey,
0,
128,
@ -187,70 +180,66 @@ describe("close_position", () => {
);
await assert.rejects(
client
.closePositionTx({
toTx(
ctx,
WhirlpoolIx.closePositionIx(ctx.program, {
positionAuthority: owner.publicKey,
receiver: owner.publicKey,
position: params.positionPda.publicKey,
positionMint: params.positionMintAddress,
positionTokenAccount: params.positionTokenAccountAddress,
positionTokenAccount: params.positionTokenAccount,
})
.buildAndExecute(),
).buildAndExecute(),
/Signature verification failed/
);
});
it("fails if delegate is not signer", async () => {
const { poolInitInfo } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo } = await initTestPool(ctx, TickSpacing.Standard);
const owner = anchor.web3.Keypair.generate();
const delegate = anchor.web3.Keypair.generate();
const { params } = await openPosition(
client,
ctx,
poolInitInfo.whirlpoolPda.publicKey,
0,
128,
owner.publicKey
);
await approveToken(
client.context.provider,
params.positionTokenAccountAddress,
delegate.publicKey,
1,
owner
);
await approveToken(ctx.provider, params.positionTokenAccount, delegate.publicKey, 1, owner);
await setAuthority(
client.context.provider,
params.positionTokenAccountAddress,
ctx.provider,
params.positionTokenAccount,
delegate.publicKey,
"CloseAccount",
owner
);
await assert.rejects(
client
.closePositionTx({
toTx(
ctx,
WhirlpoolIx.closePositionIx(ctx.program, {
positionAuthority: delegate.publicKey,
receiver: owner.publicKey,
position: params.positionPda.publicKey,
positionMint: params.positionMintAddress,
positionTokenAccount: params.positionTokenAccountAddress,
positionTokenAccount: params.positionTokenAccount,
})
.buildAndExecute(),
).buildAndExecute(),
/Signature verification failed/
);
});
it("fails if the authority does not match", async () => {
const { poolInitInfo } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo } = await initTestPool(ctx, TickSpacing.Standard);
const owner = anchor.web3.Keypair.generate();
const fakeOwner = anchor.web3.Keypair.generate();
const { params } = await openPosition(
client,
ctx,
poolInitInfo.whirlpoolPda.publicKey,
0,
128,
@ -258,14 +247,16 @@ describe("close_position", () => {
);
await assert.rejects(
client
.closePositionTx({
toTx(
ctx,
WhirlpoolIx.closePositionIx(ctx.program, {
positionAuthority: fakeOwner.publicKey,
receiver: owner.publicKey,
position: params.positionPda.publicKey,
positionMint: params.positionMintAddress,
positionTokenAccount: params.positionTokenAccountAddress,
positionTokenAccount: params.positionTokenAccount,
})
)
.addSigner(fakeOwner)
.buildAndExecute(),
/0x1783/ // MissingOrInvalidDelegate
@ -273,7 +264,7 @@ describe("close_position", () => {
});
it("fails if position token account does not contain exactly one token", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
positions: [{ tickLowerIndex: 0, tickUpperIndex: 128, liquidityAmount: ZERO_BN }],
});
@ -286,57 +277,54 @@ describe("close_position", () => {
);
await assert.rejects(
client
.closePositionTx({
toTx(
ctx,
WhirlpoolIx.closePositionIx(ctx.program, {
positionAuthority: provider.wallet.publicKey,
receiver: provider.wallet.publicKey,
position: position.publicKey,
positionMint: position.mintKeypair.publicKey,
positionTokenAccount: fakePositionTokenAccount,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
});
it("fails if delegated amount is 0", async () => {
const { poolInitInfo } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo } = await initTestPool(ctx, TickSpacing.Standard);
const owner = anchor.web3.Keypair.generate();
const delegate = anchor.web3.Keypair.generate();
const { params } = await openPosition(
client,
ctx,
poolInitInfo.whirlpoolPda.publicKey,
0,
128,
owner.publicKey
);
await approveToken(
client.context.provider,
params.positionTokenAccountAddress,
delegate.publicKey,
0,
owner
);
await approveToken(ctx.provider, params.positionTokenAccount, delegate.publicKey, 0, owner);
await setAuthority(
client.context.provider,
params.positionTokenAccountAddress,
ctx.provider,
params.positionTokenAccount,
delegate.publicKey,
"CloseAccount",
owner
);
await assert.rejects(
client
.closePositionTx({
toTx(
ctx,
WhirlpoolIx.closePositionIx(ctx.program, {
positionAuthority: delegate.publicKey,
receiver: owner.publicKey,
position: params.positionPda.publicKey,
positionMint: params.positionMintAddress,
positionTokenAccount: params.positionTokenAccountAddress,
positionTokenAccount: params.positionTokenAccount,
})
)
.addSigner(delegate)
.buildAndExecute(),
/0x1784/ // InvalidPositionTokenAmount
@ -344,44 +332,40 @@ describe("close_position", () => {
});
it("fails if positionAuthority does not match delegate", async () => {
const { poolInitInfo } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo } = await initTestPool(ctx, TickSpacing.Standard);
const owner = anchor.web3.Keypair.generate();
const delegate = anchor.web3.Keypair.generate();
const fakeDelegate = anchor.web3.Keypair.generate();
const { params } = await openPosition(
client,
ctx,
poolInitInfo.whirlpoolPda.publicKey,
0,
128,
owner.publicKey
);
await approveToken(
client.context.provider,
params.positionTokenAccountAddress,
delegate.publicKey,
1,
owner
);
await approveToken(ctx.provider, params.positionTokenAccount, delegate.publicKey, 1, owner);
await setAuthority(
client.context.provider,
params.positionTokenAccountAddress,
ctx.provider,
params.positionTokenAccount,
delegate.publicKey,
"CloseAccount",
owner
);
await assert.rejects(
client
.closePositionTx({
toTx(
ctx,
WhirlpoolIx.closePositionIx(ctx.program, {
positionAuthority: fakeDelegate.publicKey,
receiver: owner.publicKey,
position: params.positionPda.publicKey,
positionMint: params.positionMintAddress,
positionTokenAccount: params.positionTokenAccountAddress,
positionTokenAccount: params.positionTokenAccount,
})
)
.addSigner(fakeDelegate)
.buildAndExecute(),
/0x1783/ // MissingOrInvalidDelegate
@ -389,7 +373,7 @@ describe("close_position", () => {
});
it("fails if position token account mint does not match position mint", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
positions: [{ tickLowerIndex: 0, tickUpperIndex: 128, liquidityAmount: ZERO_BN }],
});
@ -402,21 +386,22 @@ describe("close_position", () => {
const fakePositionTokenAccount = await createAndMintToTokenAccount(provider, tokenMintA, 1);
await assert.rejects(
client
.closePositionTx({
toTx(
ctx,
WhirlpoolIx.closePositionIx(ctx.program, {
positionAuthority: provider.wallet.publicKey,
receiver: provider.wallet.publicKey,
position: position.publicKey,
positionMint: position.mintKeypair.publicKey,
positionTokenAccount: fakePositionTokenAccount,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
});
it("fails if position_mint does not match position's position_mint field", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
positions: [{ tickLowerIndex: 0, tickUpperIndex: 128, liquidityAmount: ZERO_BN }],
});
@ -427,15 +412,16 @@ describe("close_position", () => {
const position = positions[0];
await assert.rejects(
client
.closePositionTx({
toTx(
ctx,
WhirlpoolIx.closePositionIx(ctx.program, {
positionAuthority: provider.wallet.publicKey,
receiver: provider.wallet.publicKey,
position: position.publicKey,
positionMint: tokenMintA,
positionTokenAccount: position.tokenAccount,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7dc/ // ConstraintAddress
);
});

View File

@ -1,27 +1,38 @@
import * as assert from "assert";
import * as anchor from "@project-serum/anchor";
import { u64 } from "@solana/spl-token";
import Decimal from "decimal.js";
import {
approveToken,
WhirlpoolContext,
AccountFetcher,
PositionData,
collectFeesQuote,
TickArrayData,
TickUtil,
WhirlpoolData,
WhirlpoolIx,
PDAUtil,
TickArrayUtil,
} from "../../src";
import {
TickSpacing,
ZERO_BN,
createTokenAccount,
getTokenBalance,
TickSpacing,
approveToken,
transfer,
ZERO_BN,
} from "./utils";
import { WhirlpoolContext } from "../src/context";
import { WhirlpoolClient } from "../src/client";
import { getOraclePda, getPositionPda, getTickArrayPda, toX64 } from "../src";
import { WhirlpoolTestFixture } from "./utils/fixture";
import Decimal from "decimal.js";
import { u64 } from "@solana/spl-token";
import { initTestPool } from "./utils/init-utils";
} from "../utils";
import { WhirlpoolTestFixture } from "../utils/fixture";
import { initTestPool } from "../utils/init-utils";
import { MathUtil } from "@orca-so/common-sdk";
import { toTx } from "../../src/utils/instructions-util";
describe("collect_fees", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
it("successfully collect fees", async () => {
// In same tick array - start index 22528
@ -29,7 +40,7 @@ describe("collect_fees", () => {
const tickUpperIndex = 33536;
const tickSpacing = TickSpacing.Standard;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing,
positions: [
{ tickLowerIndex, tickUpperIndex, liquidityAmount: new u64(10_000_000) }, // In range position
@ -49,23 +60,24 @@ describe("collect_fees", () => {
positions,
} = fixture.getInfos();
const tickArrayPda = getTickArrayPda(context.program.programId, whirlpoolPda.publicKey, 22528);
const positionBeforeSwap = await client.getPosition(positions[0].publicKey);
const tickArrayPda = PDAUtil.getTickArray(ctx.program.programId, whirlpoolPda.publicKey, 22528);
const positionBeforeSwap = (await fetcher.getPosition(positions[0].publicKey)) as PositionData;
assert.ok(positionBeforeSwap.feeOwedA.eq(ZERO_BN));
assert.ok(positionBeforeSwap.feeOwedB.eq(ZERO_BN));
const oraclePda = getOraclePda(client.context.program.programId, whirlpoolPda.publicKey);
const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey);
// Accrue fees in token A
await client
.swapTx({
await toTx(
ctx,
WhirlpoolIx.swapIx(ctx.program, {
amount: new u64(200_000),
otherAmountThreshold: ZERO_BN,
sqrtPriceLimit: toX64(new Decimal(4)),
sqrtPriceLimit: MathUtil.toX64(new Decimal(4)),
amountSpecifiedIsInput: true,
aToB: true,
whirlpool: whirlpoolPda.publicKey,
tokenAuthority: context.wallet.publicKey,
tokenAuthority: ctx.wallet.publicKey,
tokenOwnerAccountA: tokenAccountA,
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenOwnerAccountB: tokenAccountB,
@ -75,18 +87,19 @@ describe("collect_fees", () => {
tickArray2: tickArrayPda.publicKey,
oracle: oraclePda.publicKey,
})
.buildAndExecute();
).buildAndExecute();
// Accrue fees in token B
await client
.swapTx({
await toTx(
ctx,
WhirlpoolIx.swapIx(ctx.program, {
amount: new u64(200_000),
otherAmountThreshold: ZERO_BN,
sqrtPriceLimit: toX64(new Decimal(5)),
sqrtPriceLimit: MathUtil.toX64(new Decimal(5)),
amountSpecifiedIsInput: true,
aToB: false,
whirlpool: whirlpoolPda.publicKey,
tokenAuthority: context.wallet.publicKey,
tokenAuthority: ctx.wallet.publicKey,
tokenOwnerAccountA: tokenAccountA,
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenOwnerAccountB: tokenAccountB,
@ -96,26 +109,44 @@ describe("collect_fees", () => {
tickArray2: tickArrayPda.publicKey,
oracle: oraclePda.publicKey,
})
.buildAndExecute();
).buildAndExecute();
await client
.updateFeesAndRewards({
await toTx(
ctx,
WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
position: positions[0].publicKey,
tickArrayLower: tickArrayPda.publicKey,
tickArrayUpper: tickArrayPda.publicKey,
})
.buildAndExecute();
).buildAndExecute();
const positionBeforeCollect = await client.getPosition(positions[0].publicKey);
const positionBeforeCollect = (await fetcher.getPosition(
positions[0].publicKey,
true
)) as PositionData;
assert.ok(positionBeforeCollect.feeOwedA.eq(new u64(581)));
assert.ok(positionBeforeCollect.feeOwedB.eq(new u64(581)));
const feeAccountA = await createTokenAccount(provider, tokenMintA, provider.wallet.publicKey);
const feeAccountB = await createTokenAccount(provider, tokenMintB, provider.wallet.publicKey);
await client
.collectFeesTx({
// Generate collect fees expectation
const whirlpoolData = (await fetcher.getPool(whirlpoolPda.publicKey)) as WhirlpoolData;
const tickArrayData = (await fetcher.getTickArray(tickArrayPda.publicKey)) as TickArrayData;
const lowerTick = TickArrayUtil.getTickFromArray(tickArrayData, tickLowerIndex, tickSpacing);
const upperTick = TickArrayUtil.getTickFromArray(tickArrayData, tickUpperIndex, tickSpacing);
const expectation = collectFeesQuote({
whirlpool: whirlpoolData,
position: positionBeforeCollect,
tickLower: lowerTick,
tickUpper: upperTick,
});
// Perform collect fees tx
await toTx(
ctx,
WhirlpoolIx.collectFeesIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: provider.wallet.publicKey,
position: positions[0].publicKey,
@ -125,32 +156,34 @@ describe("collect_fees", () => {
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenVaultB: tokenVaultBKeypair.publicKey,
})
.buildAndExecute();
const positionAfter = await client.getPosition(positions[0].publicKey);
).buildAndExecute();
const positionAfter = (await fetcher.getPosition(positions[0].publicKey, true)) as PositionData;
const feeBalanceA = await getTokenBalance(provider, feeAccountA);
const feeBalanceB = await getTokenBalance(provider, feeAccountB);
assert.equal(feeBalanceA, "581");
assert.equal(feeBalanceB, "581");
assert.equal(feeBalanceA, expectation.feeOwedA);
assert.equal(feeBalanceB, expectation.feeOwedB);
assert.ok(positionAfter.feeOwedA.eq(ZERO_BN));
assert.ok(positionAfter.feeOwedB.eq(ZERO_BN));
// Assert out of range position values
await client
.updateFeesAndRewards({
await toTx(
ctx,
WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
position: positions[1].publicKey,
tickArrayLower: positions[1].tickArrayLower,
tickArrayUpper: positions[1].tickArrayUpper,
})
.buildAndExecute();
const outOfRangePosition = await client.getPosition(positions[1].publicKey);
assert.ok(outOfRangePosition.feeOwedA.eq(ZERO_BN));
assert.ok(outOfRangePosition.feeOwedB.eq(ZERO_BN));
).buildAndExecute();
const outOfRangePosition = await fetcher.getPosition(positions[1].publicKey, true);
assert.ok(outOfRangePosition?.feeOwedA.eq(ZERO_BN));
assert.ok(outOfRangePosition?.feeOwedB.eq(ZERO_BN));
});
it("successfully collect fees with approved delegate", async () => {
const tickSpacing = TickSpacing.Standard;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing,
positions: [
{ tickLowerIndex: 0, tickUpperIndex: 128, liquidityAmount: new u64(10_000_000) }, // In range position
@ -167,8 +200,9 @@ describe("collect_fees", () => {
const delegate = anchor.web3.Keypair.generate();
await approveToken(provider, position.tokenAccount, delegate.publicKey, 1);
await client
.collectFeesTx({
await toTx(
ctx,
WhirlpoolIx.collectFeesIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: delegate.publicKey,
position: position.publicKey,
@ -178,13 +212,14 @@ describe("collect_fees", () => {
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenVaultB: tokenVaultBKeypair.publicKey,
})
)
.addSigner(delegate)
.buildAndExecute();
});
it("successfully collect fees with owner even if there is approved delegate", async () => {
const tickSpacing = TickSpacing.Standard;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing,
positions: [
{ tickLowerIndex: 0, tickUpperIndex: 128, liquidityAmount: new u64(10_000_000) }, // In range position
@ -201,8 +236,9 @@ describe("collect_fees", () => {
const delegate = anchor.web3.Keypair.generate();
await approveToken(provider, position.tokenAccount, delegate.publicKey, 1);
await client
.collectFeesTx({
await toTx(
ctx,
WhirlpoolIx.collectFeesIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: provider.wallet.publicKey,
position: position.publicKey,
@ -212,12 +248,12 @@ describe("collect_fees", () => {
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenVaultB: tokenVaultBKeypair.publicKey,
})
.buildAndExecute();
).buildAndExecute();
});
it("successfully collect fees with transferred position token", async () => {
const tickSpacing = TickSpacing.Standard;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing,
positions: [
{ tickLowerIndex: 0, tickUpperIndex: 128, liquidityAmount: new u64(10_000_000) }, // In range position
@ -240,8 +276,9 @@ describe("collect_fees", () => {
await transfer(provider, position.tokenAccount, newOwnerPositionTokenAccount, 1);
await client
.collectFeesTx({
await toTx(
ctx,
WhirlpoolIx.collectFeesIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: newOwner.publicKey,
position: position.publicKey,
@ -251,6 +288,7 @@ describe("collect_fees", () => {
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenVaultB: tokenVaultBKeypair.publicKey,
})
)
.addSigner(newOwner)
.buildAndExecute();
});
@ -260,7 +298,7 @@ describe("collect_fees", () => {
const tickLowerIndex = 29440;
const tickUpperIndex = 33536;
const tickSpacing = TickSpacing.Standard;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing,
positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: new u64(10_000_000) }],
});
@ -273,11 +311,12 @@ describe("collect_fees", () => {
const {
poolInitInfo: { whirlpoolPda: whirlpoolPda2 },
} = await initTestPool(client, tickSpacing);
} = await initTestPool(ctx, tickSpacing);
await assert.rejects(
client
.collectFeesTx({
toTx(
ctx,
WhirlpoolIx.collectFeesIx(ctx.program, {
whirlpool: whirlpoolPda2.publicKey,
positionAuthority: provider.wallet.publicKey,
position: positions[0].publicKey,
@ -287,7 +326,7 @@ describe("collect_fees", () => {
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenVaultB: tokenVaultBKeypair.publicKey,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d1/ // ConstraintHasOne
);
});
@ -297,7 +336,7 @@ describe("collect_fees", () => {
const tickLowerIndex = 29440;
const tickUpperIndex = 33536;
const tickSpacing = TickSpacing.Standard;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing,
positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: new u64(10_000_000) }],
});
@ -315,8 +354,9 @@ describe("collect_fees", () => {
);
await assert.rejects(
client
.collectFeesTx({
toTx(
ctx,
WhirlpoolIx.collectFeesIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: provider.wallet.publicKey,
position: positions[0].publicKey,
@ -326,15 +366,16 @@ describe("collect_fees", () => {
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenVaultB: tokenVaultBKeypair.publicKey,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
await transfer(provider, positions[0].tokenAccount, positionTokenAccount2, 1);
await assert.rejects(
client
.collectFeesTx({
toTx(
ctx,
WhirlpoolIx.collectFeesIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: provider.wallet.publicKey,
position: positions[0].publicKey,
@ -344,7 +385,7 @@ describe("collect_fees", () => {
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenVaultB: tokenVaultBKeypair.publicKey,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
});
@ -354,7 +395,7 @@ describe("collect_fees", () => {
const tickLowerIndex = 29440;
const tickUpperIndex = 33536;
const tickSpacing = TickSpacing.Standard;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing,
positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: new u64(10_000_000) }],
});
@ -368,8 +409,9 @@ describe("collect_fees", () => {
const delegate = anchor.web3.Keypair.generate();
await assert.rejects(
client
.collectFeesTx({
toTx(
ctx,
WhirlpoolIx.collectFeesIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: delegate.publicKey,
position: positions[0].publicKey,
@ -379,6 +421,7 @@ describe("collect_fees", () => {
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenVaultB: tokenVaultBKeypair.publicKey,
})
)
.addSigner(delegate)
.buildAndExecute(),
/0x1783/ // MissingOrInvalidDelegate
@ -390,7 +433,7 @@ describe("collect_fees", () => {
const tickLowerIndex = 29440;
const tickUpperIndex = 33536;
const tickSpacing = TickSpacing.Standard;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing,
positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: new u64(10_000_000) }],
});
@ -405,8 +448,9 @@ describe("collect_fees", () => {
await approveToken(provider, positions[0].tokenAccount, delegate.publicKey, 2);
await assert.rejects(
client
.collectFeesTx({
toTx(
ctx,
WhirlpoolIx.collectFeesIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: delegate.publicKey,
position: positions[0].publicKey,
@ -416,6 +460,7 @@ describe("collect_fees", () => {
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenVaultB: tokenVaultBKeypair.publicKey,
})
)
.addSigner(delegate)
.buildAndExecute(),
/0x1784/ // InvalidPositionTokenAmount
@ -427,7 +472,7 @@ describe("collect_fees", () => {
const tickLowerIndex = 29440;
const tickUpperIndex = 33536;
const tickSpacing = TickSpacing.Standard;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing,
positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: new u64(10_000_000) }],
});
@ -442,8 +487,9 @@ describe("collect_fees", () => {
await approveToken(provider, positions[0].tokenAccount, delegate.publicKey, 1);
await assert.rejects(
client
.collectFeesTx({
toTx(
ctx,
WhirlpoolIx.collectFeesIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: delegate.publicKey,
position: positions[0].publicKey,
@ -453,7 +499,7 @@ describe("collect_fees", () => {
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenVaultB: tokenVaultBKeypair.publicKey,
})
.buildAndExecute(),
).buildAndExecute(),
/Signature verification failed/
);
});
@ -463,7 +509,7 @@ describe("collect_fees", () => {
const tickLowerIndex = 29440;
const tickUpperIndex = 33536;
const tickSpacing = TickSpacing.Standard;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing,
positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: new u64(10_000_000) }],
});
@ -481,8 +527,9 @@ describe("collect_fees", () => {
);
await assert.rejects(
client
.collectFeesTx({
toTx(
ctx,
WhirlpoolIx.collectFeesIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: provider.wallet.publicKey,
position: positions[0].publicKey,
@ -492,7 +539,7 @@ describe("collect_fees", () => {
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenVaultB: tokenVaultBKeypair.publicKey,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
});
@ -502,7 +549,7 @@ describe("collect_fees", () => {
const tickLowerIndex = 29440;
const tickUpperIndex = 33536;
const tickSpacing = TickSpacing.Standard;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing,
positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: new u64(10_000_000) }],
});
@ -523,8 +570,9 @@ describe("collect_fees", () => {
const fakeVaultB = await createTokenAccount(provider, tokenMintB, provider.wallet.publicKey);
await assert.rejects(
client
.collectFeesTx({
toTx(
ctx,
WhirlpoolIx.collectFeesIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: provider.wallet.publicKey,
position: positions[0].publicKey,
@ -534,13 +582,14 @@ describe("collect_fees", () => {
tokenVaultA: fakeVaultA,
tokenVaultB: tokenVaultBKeypair.publicKey,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7dc/ // ConstraintAddress
);
await assert.rejects(
client
.collectFeesTx({
toTx(
ctx,
WhirlpoolIx.collectFeesIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: provider.wallet.publicKey,
position: positions[0].publicKey,
@ -550,7 +599,7 @@ describe("collect_fees", () => {
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenVaultB: fakeVaultB,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7dc/ // ConstraintAddress
);
});
@ -560,7 +609,7 @@ describe("collect_fees", () => {
const tickLowerIndex = 29440;
const tickUpperIndex = 33536;
const tickSpacing = TickSpacing.Standard;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing,
positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: new u64(10_000_000) }],
});
@ -589,8 +638,9 @@ describe("collect_fees", () => {
);
await assert.rejects(
client
.collectFeesTx({
toTx(
ctx,
WhirlpoolIx.collectFeesIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: provider.wallet.publicKey,
position: positions[0].publicKey,
@ -600,13 +650,14 @@ describe("collect_fees", () => {
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenVaultB: tokenVaultBKeypair.publicKey,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
await assert.rejects(
client
.collectFeesTx({
toTx(
ctx,
WhirlpoolIx.collectFeesIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: provider.wallet.publicKey,
position: positions[0].publicKey,
@ -616,7 +667,7 @@ describe("collect_fees", () => {
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenVaultB: tokenVaultBKeypair.publicKey,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
});

View File

@ -1,20 +1,20 @@
import * as assert from "assert";
import * as anchor from "@project-serum/anchor";
import { WhirlpoolContext } from "../src/context";
import { WhirlpoolClient } from "../src/client";
import { initTestPool } from "./utils/init-utils";
import { getOraclePda, toX64 } from "../src";
import { WhirlpoolTestFixture } from "./utils/fixture";
import { u64 } from "@solana/spl-token";
import Decimal from "decimal.js";
import { createTokenAccount, getTokenBalance, TickSpacing, ZERO_BN } from "./utils";
import { WhirlpoolContext, AccountFetcher, WhirlpoolData, WhirlpoolIx, PDAUtil } from "../../src";
import { TickSpacing, ZERO_BN, createTokenAccount, getTokenBalance } from "../utils";
import { WhirlpoolTestFixture } from "../utils/fixture";
import { initTestPool } from "../utils/init-utils";
import { MathUtil } from "@orca-so/common-sdk";
import { toTx } from "../../src/utils/instructions-util";
describe("collect_protocol_fees", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
it("successfully collects fees", async () => {
// In same tick array - start index 22528
@ -22,7 +22,7 @@ describe("collect_protocol_fees", () => {
const tickUpperIndex = 33536;
const tickSpacing = TickSpacing.Standard;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing,
positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: new u64(10_000_000) }],
});
@ -35,40 +35,43 @@ describe("collect_protocol_fees", () => {
tokenMintB,
},
configKeypairs: { feeAuthorityKeypair, collectProtocolFeesAuthorityKeypair },
configInitInfo: { whirlpoolConfigKeypair },
configInitInfo: { whirlpoolsConfigKeypair: whirlpoolsConfigKeypair },
tokenAccountA,
tokenAccountB,
positions,
} = fixture.getInfos();
await client
.setProtocolFeeRateIx({
await toTx(
ctx,
WhirlpoolIx.setProtocolFeeRateIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
whirlpoolsConfig: whirlpoolConfigKeypair.publicKey,
whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey,
feeAuthority: feeAuthorityKeypair.publicKey,
protocolFeeRate: 2500,
})
)
.addSigner(feeAuthorityKeypair)
.buildAndExecute();
const poolBefore = await client.getPool(whirlpoolPda.publicKey);
assert.ok(poolBefore.protocolFeeOwedA.eq(ZERO_BN));
assert.ok(poolBefore.protocolFeeOwedB.eq(ZERO_BN));
const poolBefore = (await fetcher.getPool(whirlpoolPda.publicKey, true)) as WhirlpoolData;
assert.ok(poolBefore?.protocolFeeOwedA.eq(ZERO_BN));
assert.ok(poolBefore?.protocolFeeOwedB.eq(ZERO_BN));
const tickArrayPda = positions[0].tickArrayLower;
const oraclePda = getOraclePda(client.context.program.programId, whirlpoolPda.publicKey);
const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey);
// Accrue fees in token A
await client
.swapTx({
await toTx(
ctx,
WhirlpoolIx.swapIx(ctx.program, {
amount: new u64(200_000),
otherAmountThreshold: ZERO_BN,
sqrtPriceLimit: toX64(new Decimal(4)),
sqrtPriceLimit: MathUtil.toX64(new Decimal(4)),
amountSpecifiedIsInput: true,
aToB: true,
whirlpool: whirlpoolPda.publicKey,
tokenAuthority: context.wallet.publicKey,
tokenAuthority: ctx.wallet.publicKey,
tokenOwnerAccountA: tokenAccountA,
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenOwnerAccountB: tokenAccountB,
@ -78,18 +81,19 @@ describe("collect_protocol_fees", () => {
tickArray2: tickArrayPda,
oracle: oraclePda.publicKey,
})
.buildAndExecute();
).buildAndExecute();
// Accrue fees in token B
await client
.swapTx({
await toTx(
ctx,
WhirlpoolIx.swapIx(ctx.program, {
amount: new u64(200_000),
otherAmountThreshold: ZERO_BN,
sqrtPriceLimit: toX64(new Decimal(5)),
sqrtPriceLimit: MathUtil.toX64(new Decimal(5)),
amountSpecifiedIsInput: true,
aToB: false,
whirlpool: whirlpoolPda.publicKey,
tokenAuthority: context.wallet.publicKey,
tokenAuthority: ctx.wallet.publicKey,
tokenOwnerAccountA: tokenAccountA,
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenOwnerAccountB: tokenAccountB,
@ -99,25 +103,27 @@ describe("collect_protocol_fees", () => {
tickArray2: tickArrayPda,
oracle: oraclePda.publicKey,
})
.buildAndExecute();
).buildAndExecute();
const poolAfter = await client.getPool(whirlpoolPda.publicKey);
assert.ok(poolAfter.protocolFeeOwedA.eq(new u64(150)));
assert.ok(poolAfter.protocolFeeOwedB.eq(new u64(150)));
const poolAfter = (await fetcher.getPool(whirlpoolPda.publicKey, true)) as WhirlpoolData;
assert.ok(poolAfter?.protocolFeeOwedA.eq(new u64(150)));
assert.ok(poolAfter?.protocolFeeOwedB.eq(new u64(150)));
const destA = await createTokenAccount(provider, tokenMintA, provider.wallet.publicKey);
const destB = await createTokenAccount(provider, tokenMintB, provider.wallet.publicKey);
await client
.collectProtocolFeesTx({
whirlpoolsConfig: whirlpoolConfigKeypair.publicKey,
await toTx(
ctx,
WhirlpoolIx.collectProtocolFeesIx(ctx.program, {
whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey,
whirlpool: whirlpoolPda.publicKey,
collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey,
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenVaultB: tokenVaultBKeypair.publicKey,
tokenDestinationA: destA,
tokenDestinationB: destB,
tokenOwnerAccountA: destA,
tokenOwnerAccountB: destB,
})
)
.addSigner(collectProtocolFeesAuthorityKeypair)
.buildAndExecute();
@ -125,13 +131,13 @@ describe("collect_protocol_fees", () => {
const balanceDestB = await getTokenBalance(provider, destB);
assert.equal(balanceDestA, "150");
assert.equal(balanceDestB, "150");
assert.ok(poolBefore.protocolFeeOwedA.eq(ZERO_BN));
assert.ok(poolBefore.protocolFeeOwedB.eq(ZERO_BN));
assert.ok(poolBefore?.protocolFeeOwedA.eq(ZERO_BN));
assert.ok(poolBefore?.protocolFeeOwedB.eq(ZERO_BN));
});
it("fails to collect fees without the authority's signature", async () => {
const tickSpacing = TickSpacing.Standard;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing,
positions: [
{ tickLowerIndex: 29440, tickUpperIndex: 33536, liquidityAmount: new u64(10_000_000) },
@ -140,30 +146,31 @@ describe("collect_protocol_fees", () => {
const {
poolInitInfo: { whirlpoolPda, tokenVaultAKeypair, tokenVaultBKeypair },
configKeypairs: { collectProtocolFeesAuthorityKeypair },
configInitInfo: { whirlpoolConfigKeypair },
configInitInfo: { whirlpoolsConfigKeypair },
tokenAccountA,
tokenAccountB,
} = fixture.getInfos();
await assert.rejects(
client
.collectProtocolFeesTx({
whirlpoolsConfig: whirlpoolConfigKeypair.publicKey,
toTx(
ctx,
WhirlpoolIx.collectProtocolFeesIx(ctx.program, {
whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey,
whirlpool: whirlpoolPda.publicKey,
collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey,
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenVaultB: tokenVaultBKeypair.publicKey,
tokenDestinationA: tokenAccountA,
tokenDestinationB: tokenAccountB,
tokenOwnerAccountA: tokenAccountA,
tokenOwnerAccountB: tokenAccountB,
})
.buildAndExecute(),
).buildAndExecute(),
/Signature verification failed/
);
});
it("fails when collect_protocol_fees_authority is invalid", async () => {
const tickSpacing = TickSpacing.Standard;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing,
positions: [
{ tickLowerIndex: 29440, tickUpperIndex: 33536, liquidityAmount: new u64(10_000_000) },
@ -172,22 +179,24 @@ describe("collect_protocol_fees", () => {
const {
poolInitInfo: { whirlpoolPda, tokenVaultAKeypair, tokenVaultBKeypair },
configKeypairs: { rewardEmissionsSuperAuthorityKeypair },
configInitInfo: { whirlpoolConfigKeypair },
configInitInfo: { whirlpoolsConfigKeypair },
tokenAccountA,
tokenAccountB,
} = fixture.getInfos();
await assert.rejects(
client
.collectProtocolFeesTx({
whirlpoolsConfig: whirlpoolConfigKeypair.publicKey,
toTx(
ctx,
WhirlpoolIx.collectProtocolFeesIx(ctx.program, {
whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey,
whirlpool: whirlpoolPda.publicKey,
collectProtocolFeesAuthority: rewardEmissionsSuperAuthorityKeypair.publicKey,
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenVaultB: tokenVaultBKeypair.publicKey,
tokenDestinationA: tokenAccountA,
tokenDestinationB: tokenAccountB,
tokenOwnerAccountA: tokenAccountA,
tokenOwnerAccountB: tokenAccountB,
})
)
.addSigner(rewardEmissionsSuperAuthorityKeypair)
.buildAndExecute(),
/0x7dc/ // ConstraintAddress
@ -196,7 +205,7 @@ describe("collect_protocol_fees", () => {
it("fails when whirlpool does not match config", async () => {
const tickSpacing = TickSpacing.Standard;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing,
positions: [
{ tickLowerIndex: 29440, tickUpperIndex: 33536, liquidityAmount: new u64(10_000_000) },
@ -205,25 +214,27 @@ describe("collect_protocol_fees", () => {
const {
poolInitInfo: { tokenVaultAKeypair, tokenVaultBKeypair },
configKeypairs: { collectProtocolFeesAuthorityKeypair },
configInitInfo: { whirlpoolConfigKeypair },
configInitInfo: { whirlpoolsConfigKeypair },
tokenAccountA,
tokenAccountB,
} = fixture.getInfos();
const {
poolInitInfo: { whirlpoolPda: whirlpoolPda2 },
} = await initTestPool(client, tickSpacing);
} = await initTestPool(ctx, tickSpacing);
await assert.rejects(
client
.collectProtocolFeesTx({
whirlpoolsConfig: whirlpoolConfigKeypair.publicKey,
toTx(
ctx,
WhirlpoolIx.collectProtocolFeesIx(ctx.program, {
whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey,
whirlpool: whirlpoolPda2.publicKey,
collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey,
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenVaultB: tokenVaultBKeypair.publicKey,
tokenDestinationA: tokenAccountA,
tokenDestinationB: tokenAccountB,
tokenOwnerAccountA: tokenAccountA,
tokenOwnerAccountB: tokenAccountB,
})
)
.addSigner(collectProtocolFeesAuthorityKeypair)
.buildAndExecute(),
/0x7d1/ // ConstraintHasOne
@ -232,7 +243,7 @@ describe("collect_protocol_fees", () => {
it("fails when vaults do not match whirlpool vaults", async () => {
const tickSpacing = TickSpacing.Standard;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing,
positions: [
{ tickLowerIndex: 29440, tickUpperIndex: 33536, liquidityAmount: new u64(10_000_000) },
@ -247,7 +258,7 @@ describe("collect_protocol_fees", () => {
tokenMintB,
},
configKeypairs: { collectProtocolFeesAuthorityKeypair },
configInitInfo: { whirlpoolConfigKeypair },
configInitInfo: { whirlpoolsConfigKeypair: whirlpoolsConfigKeypair },
tokenAccountA,
tokenAccountB,
} = fixture.getInfos();
@ -256,32 +267,36 @@ describe("collect_protocol_fees", () => {
const fakeVaultB = await createTokenAccount(provider, tokenMintB, provider.wallet.publicKey);
await assert.rejects(
client
.collectProtocolFeesTx({
whirlpoolsConfig: whirlpoolConfigKeypair.publicKey,
toTx(
ctx,
WhirlpoolIx.collectProtocolFeesIx(ctx.program, {
whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey,
whirlpool: whirlpoolPda.publicKey,
collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey,
tokenVaultA: fakeVaultA,
tokenVaultB: tokenVaultBKeypair.publicKey,
tokenDestinationA: tokenAccountA,
tokenDestinationB: tokenAccountB,
tokenOwnerAccountA: tokenAccountA,
tokenOwnerAccountB: tokenAccountB,
})
)
.addSigner(collectProtocolFeesAuthorityKeypair)
.buildAndExecute(),
/0x7dc/ // ConstraintAddress
);
await assert.rejects(
client
.collectProtocolFeesTx({
whirlpoolsConfig: whirlpoolConfigKeypair.publicKey,
toTx(
ctx,
WhirlpoolIx.collectProtocolFeesIx(ctx.program, {
whirlpoolsConfig: whirlpoolsConfigKeypair.publicKey,
whirlpool: whirlpoolPda.publicKey,
collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey,
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenVaultB: fakeVaultB,
tokenDestinationA: tokenAccountA,
tokenDestinationB: tokenAccountB,
tokenOwnerAccountA: tokenAccountA,
tokenOwnerAccountB: tokenAccountB,
})
)
.addSigner(collectProtocolFeesAuthorityKeypair)
.buildAndExecute(),
/0x7dc/ // ConstraintAddress
@ -290,7 +305,7 @@ describe("collect_protocol_fees", () => {
it("fails when destination mints do not match whirlpool mints", async () => {
const tickSpacing = TickSpacing.Standard;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing,
positions: [
{ tickLowerIndex: 29440, tickUpperIndex: 33536, liquidityAmount: new u64(10_000_000) },
@ -305,7 +320,7 @@ describe("collect_protocol_fees", () => {
tokenMintB,
},
configKeypairs: { collectProtocolFeesAuthorityKeypair },
configInitInfo: { whirlpoolConfigKeypair },
configInitInfo: { whirlpoolsConfigKeypair: whirlpoolsConfigKepair },
tokenAccountA,
tokenAccountB,
} = fixture.getInfos();
@ -314,32 +329,36 @@ describe("collect_protocol_fees", () => {
const invalidDestB = await createTokenAccount(provider, tokenMintA, provider.wallet.publicKey);
await assert.rejects(
client
.collectProtocolFeesTx({
whirlpoolsConfig: whirlpoolConfigKeypair.publicKey,
toTx(
ctx,
WhirlpoolIx.collectProtocolFeesIx(ctx.program, {
whirlpoolsConfig: whirlpoolsConfigKepair.publicKey,
whirlpool: whirlpoolPda.publicKey,
collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey,
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenVaultB: tokenVaultBKeypair.publicKey,
tokenDestinationA: invalidDestA,
tokenDestinationB: tokenAccountB,
tokenOwnerAccountA: invalidDestA,
tokenOwnerAccountB: tokenAccountB,
})
)
.addSigner(collectProtocolFeesAuthorityKeypair)
.buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
await assert.rejects(
client
.collectProtocolFeesTx({
whirlpoolsConfig: whirlpoolConfigKeypair.publicKey,
toTx(
ctx,
WhirlpoolIx.collectProtocolFeesIx(ctx.program, {
whirlpoolsConfig: whirlpoolsConfigKepair.publicKey,
whirlpool: whirlpoolPda.publicKey,
collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey,
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenVaultB: tokenVaultBKeypair.publicKey,
tokenDestinationA: tokenAccountA,
tokenDestinationB: invalidDestB,
tokenOwnerAccountA: tokenAccountA,
tokenOwnerAccountB: invalidDestB,
})
)
.addSigner(collectProtocolFeesAuthorityKeypair)
.buildAndExecute(),
/0x7d3/ // ConstraintRaw

View File

@ -1,43 +1,70 @@
import * as assert from "assert";
import * as anchor from "@project-serum/anchor";
import { u64 } from "@solana/spl-token";
import Decimal from "decimal.js";
import {
approveToken,
createAndMintToTokenAccount,
createMint,
WhirlpoolContext,
AccountFetcher,
NUM_REWARDS,
collectRewardsQuote,
WhirlpoolData,
PositionData,
TickArrayData,
TickUtil,
WhirlpoolIx,
TickArrayUtil,
} from "../../src";
import {
TickSpacing,
sleep,
createTokenAccount,
getTokenBalance,
sleep,
TickSpacing,
transfer,
ZERO_BN,
} from "./utils";
import { WhirlpoolContext } from "../src/context";
import { WhirlpoolClient } from "../src/client";
import { u64 } from "@solana/spl-token";
import { NUM_REWARDS, toX64 } from "../src";
import Decimal from "decimal.js";
import { WhirlpoolTestFixture } from "./utils/fixture";
import { initTestPool } from "./utils/init-utils";
approveToken,
transfer,
createMint,
createAndMintToTokenAccount,
} from "../utils";
import { WhirlpoolTestFixture } from "../utils/fixture";
import { initTestPool } from "../utils/init-utils";
import { MathUtil } from "@orca-so/common-sdk";
import { toTx } from "../../src/utils/instructions-util";
describe("collect_reward", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
it("successfully collect rewards", async () => {
const vaultStartBalance = 1_000_000;
const fixture = await new WhirlpoolTestFixture(client).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(1)),
const lowerTickIndex = -1280,
upperTickIndex = 1280,
tickSpacing = TickSpacing.Standard;
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: tickSpacing,
initialSqrtPrice: MathUtil.toX64(new Decimal(1)),
positions: [
{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: new anchor.BN(1_000_000) },
{
tickLowerIndex: lowerTickIndex,
tickUpperIndex: upperTickIndex,
liquidityAmount: new anchor.BN(1_000_000),
},
],
rewards: [
{ emissionsPerSecondX64: toX64(new Decimal(10)), vaultAmount: new u64(vaultStartBalance) },
{ emissionsPerSecondX64: toX64(new Decimal(10)), vaultAmount: new u64(vaultStartBalance) },
{ emissionsPerSecondX64: toX64(new Decimal(10)), vaultAmount: new u64(vaultStartBalance) },
{
emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)),
vaultAmount: new u64(vaultStartBalance),
},
{
emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)),
vaultAmount: new u64(vaultStartBalance),
},
{
emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)),
vaultAmount: new u64(vaultStartBalance),
},
],
});
const {
@ -46,23 +73,52 @@ describe("collect_reward", () => {
rewards,
} = fixture.getInfos();
await sleep(500);
await client
.updateFeesAndRewards({
await toTx(
ctx,
WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
position: positions[0].publicKey,
tickArrayLower: positions[0].tickArrayLower,
tickArrayUpper: positions[0].tickArrayUpper,
})
.buildAndExecute();
).buildAndExecute();
// Generate collect reward expectation
const pool = (await fetcher.getPool(whirlpoolPda.publicKey, true)) as WhirlpoolData;
const positionPreCollect = (await fetcher.getPosition(
positions[0].publicKey,
true
)) as PositionData;
const tickArrayLower = (await fetcher.getTickArray(
positions[0].tickArrayLower,
true
)) as TickArrayData;
const tickArrayUpper = (await fetcher.getTickArray(
positions[0].tickArrayUpper,
true
)) as TickArrayData;
const lowerTick = TickArrayUtil.getTickFromArray(tickArrayLower, lowerTickIndex, tickSpacing);
const upperTick = TickArrayUtil.getTickFromArray(tickArrayUpper, upperTickIndex, tickSpacing);
const expectation = collectRewardsQuote({
whirlpool: pool,
position: positionPreCollect,
tickLower: lowerTick,
tickUpper: upperTick,
});
// Perform collect rewards tx
for (let i = 0; i < NUM_REWARDS; i++) {
const rewardOwnerAccount = await createTokenAccount(
provider,
rewards[i].rewardMint,
provider.wallet.publicKey
);
await client
.collectRewardTx({
await toTx(
ctx,
WhirlpoolIx.collectRewardIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: provider.wallet.publicKey,
position: positions[0].publicKey,
@ -71,30 +127,33 @@ describe("collect_reward", () => {
rewardVault: rewards[i].rewardVaultKeypair.publicKey,
rewardIndex: i,
})
.buildAndExecute();
).buildAndExecute();
const collectedBalance = parseInt(await getTokenBalance(provider, rewardOwnerAccount));
assert.ok(collectedBalance > 0);
assert.ok(collectedBalance === expectation[i]?.toNumber());
const vaultBalance = parseInt(
await getTokenBalance(provider, rewards[i].rewardVaultKeypair.publicKey)
);
assert.equal(vaultStartBalance - collectedBalance, vaultBalance);
const position = await client.getPosition(positions[0].publicKey);
assert.equal(position.rewardInfos[i].amountOwed, 0);
assert.ok(position.rewardInfos[i].growthInsideCheckpoint.gte(ZERO_BN));
const position = await fetcher.getPosition(positions[0].publicKey, true);
assert.equal(position?.rewardInfos[i].amountOwed, 0);
assert.ok(position?.rewardInfos[i].growthInsideCheckpoint.gte(ZERO_BN));
}
});
it("successfully collect reward with a position authority delegate", async () => {
const vaultStartBalance = 1_000_000;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(1)),
initialSqrtPrice: MathUtil.toX64(new Decimal(1)),
positions: [
{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: new anchor.BN(1_000_000) },
],
rewards: [
{ emissionsPerSecondX64: toX64(new Decimal(2)), vaultAmount: new u64(vaultStartBalance) },
{
emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)),
vaultAmount: new u64(vaultStartBalance),
},
],
});
const {
@ -108,20 +167,22 @@ describe("collect_reward", () => {
provider.wallet.publicKey
);
await client
.updateFeesAndRewards({
await toTx(
ctx,
WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
position: positions[0].publicKey,
tickArrayLower: positions[0].tickArrayLower,
tickArrayUpper: positions[0].tickArrayUpper,
})
.buildAndExecute();
).buildAndExecute();
const delegate = anchor.web3.Keypair.generate();
await approveToken(provider, positions[0].tokenAccount, delegate.publicKey, 1);
await client
.collectRewardTx({
await toTx(
ctx,
WhirlpoolIx.collectRewardIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: delegate.publicKey,
position: positions[0].publicKey,
@ -130,20 +191,24 @@ describe("collect_reward", () => {
rewardVault: rewards[0].rewardVaultKeypair.publicKey,
rewardIndex: 0,
})
)
.addSigner(delegate)
.buildAndExecute();
});
it("successfully collect reward with transferred position token", async () => {
const vaultStartBalance = 1_000_000;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(1)),
initialSqrtPrice: MathUtil.toX64(new Decimal(1)),
positions: [
{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: new anchor.BN(1_000_000) },
],
rewards: [
{ emissionsPerSecondX64: toX64(new Decimal(2)), vaultAmount: new u64(vaultStartBalance) },
{
emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)),
vaultAmount: new u64(vaultStartBalance),
},
],
});
const {
@ -165,17 +230,19 @@ describe("collect_reward", () => {
);
await transfer(provider, positions[0].tokenAccount, delegatePositionAccount, 1);
await client
.updateFeesAndRewards({
await toTx(
ctx,
WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
position: positions[0].publicKey,
tickArrayLower: positions[0].tickArrayLower,
tickArrayUpper: positions[0].tickArrayUpper,
})
.buildAndExecute();
).buildAndExecute();
await client
.collectRewardTx({
await toTx(
ctx,
WhirlpoolIx.collectRewardIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: delegate.publicKey,
position: positions[0].publicKey,
@ -184,20 +251,24 @@ describe("collect_reward", () => {
rewardVault: rewards[0].rewardVaultKeypair.publicKey,
rewardIndex: 0,
})
)
.addSigner(delegate)
.buildAndExecute();
});
it("successfully collect reward with owner even when there is a delegate", async () => {
const vaultStartBalance = 1_000_000;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(1)),
initialSqrtPrice: MathUtil.toX64(new Decimal(1)),
positions: [
{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: new anchor.BN(1_000_000) },
],
rewards: [
{ emissionsPerSecondX64: toX64(new Decimal(2)), vaultAmount: new u64(vaultStartBalance) },
{
emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)),
vaultAmount: new u64(vaultStartBalance),
},
],
});
const {
@ -211,20 +282,22 @@ describe("collect_reward", () => {
provider.wallet.publicKey
);
await client
.updateFeesAndRewards({
await toTx(
ctx,
WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
position: positions[0].publicKey,
tickArrayLower: positions[0].tickArrayLower,
tickArrayUpper: positions[0].tickArrayUpper,
})
.buildAndExecute();
).buildAndExecute();
const delegate = anchor.web3.Keypair.generate();
await approveToken(provider, positions[0].tokenAccount, delegate.publicKey, 1);
await client
.collectRewardTx({
await toTx(
ctx,
WhirlpoolIx.collectRewardIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: provider.wallet.publicKey,
position: positions[0].publicKey,
@ -233,13 +306,13 @@ describe("collect_reward", () => {
rewardVault: rewards[0].rewardVaultKeypair.publicKey,
rewardIndex: 0,
})
.buildAndExecute();
).buildAndExecute();
});
it("fails when reward index references an uninitialized reward", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(1)),
initialSqrtPrice: MathUtil.toX64(new Decimal(1)),
positions: [
{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: new anchor.BN(1_000_000) },
],
@ -256,8 +329,9 @@ describe("collect_reward", () => {
);
await assert.rejects(
client
.collectRewardTx({
toTx(
ctx,
WhirlpoolIx.collectRewardIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: provider.wallet.publicKey,
position: positions[0].publicKey,
@ -266,33 +340,36 @@ describe("collect_reward", () => {
rewardVault: anchor.web3.PublicKey.default,
rewardIndex: 0,
})
.buildAndExecute(),
).buildAndExecute(),
/0xbbf/ // AccountNotInitialized
);
});
it("fails when position does not match whirlpool", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(1)),
initialSqrtPrice: MathUtil.toX64(new Decimal(1)),
positions: [
{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: new anchor.BN(1_000_000) },
],
rewards: [{ emissionsPerSecondX64: toX64(new Decimal(2)), vaultAmount: new u64(1_000_000) }],
rewards: [
{ emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)), vaultAmount: new u64(1_000_000) },
],
});
const { positions, rewards } = fixture.getInfos();
const {
poolInitInfo: { whirlpoolPda },
} = await initTestPool(client, TickSpacing.Standard);
} = await initTestPool(ctx, TickSpacing.Standard);
const rewardOwnerAccount = await createTokenAccount(
provider,
rewards[0].rewardMint,
provider.wallet.publicKey
);
await assert.rejects(
client
.collectRewardTx({
toTx(
ctx,
WhirlpoolIx.collectRewardIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: provider.wallet.publicKey,
position: positions[0].publicKey,
@ -301,19 +378,21 @@ describe("collect_reward", () => {
rewardVault: rewards[0].rewardVaultKeypair.publicKey,
rewardIndex: 0,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d1/ // ConstraintHasOne
);
});
it("fails when position token account does not have exactly one token", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(1)),
initialSqrtPrice: MathUtil.toX64(new Decimal(1)),
positions: [
{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: new anchor.BN(1_000_000) },
],
rewards: [{ emissionsPerSecondX64: toX64(new Decimal(2)), vaultAmount: new u64(1_000_000) }],
rewards: [
{ emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)), vaultAmount: new u64(1_000_000) },
],
});
const {
poolInitInfo: { whirlpoolPda },
@ -333,8 +412,9 @@ describe("collect_reward", () => {
);
await transfer(provider, positions[0].tokenAccount, otherPositionAcount, 1);
await assert.rejects(
client
.collectRewardTx({
toTx(
ctx,
WhirlpoolIx.collectRewardIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: provider.wallet.publicKey,
position: positions[0].publicKey,
@ -343,19 +423,21 @@ describe("collect_reward", () => {
rewardVault: rewards[0].rewardVaultKeypair.publicKey,
rewardIndex: 0,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
});
it("fails when position token account mint does not match position mint", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(1)),
initialSqrtPrice: MathUtil.toX64(new Decimal(1)),
positions: [
{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: new anchor.BN(1_000_000) },
],
rewards: [{ emissionsPerSecondX64: toX64(new Decimal(2)), vaultAmount: new u64(1_000_000) }],
rewards: [
{ emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)), vaultAmount: new u64(1_000_000) },
],
});
const {
poolInitInfo: { whirlpoolPda, tokenMintA },
@ -371,8 +453,9 @@ describe("collect_reward", () => {
const fakePositionTokenAccount = await createAndMintToTokenAccount(provider, tokenMintA, 1);
await assert.rejects(
client
.collectRewardTx({
toTx(
ctx,
WhirlpoolIx.collectRewardIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: provider.wallet.publicKey,
position: positions[0].publicKey,
@ -381,19 +464,21 @@ describe("collect_reward", () => {
rewardVault: rewards[0].rewardVaultKeypair.publicKey,
rewardIndex: 0,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
});
it("fails when position authority is not approved delegate for position token account", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(1)),
initialSqrtPrice: MathUtil.toX64(new Decimal(1)),
positions: [
{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: new anchor.BN(1_000_000) },
],
rewards: [{ emissionsPerSecondX64: toX64(new Decimal(2)), vaultAmount: new u64(1_000_000) }],
rewards: [
{ emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)), vaultAmount: new u64(1_000_000) },
],
});
const {
poolInitInfo: { whirlpoolPda },
@ -407,8 +492,9 @@ describe("collect_reward", () => {
);
const delegate = anchor.web3.Keypair.generate();
await assert.rejects(
client
.collectRewardTx({
toTx(
ctx,
WhirlpoolIx.collectRewardIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: delegate.publicKey,
position: positions[0].publicKey,
@ -417,6 +503,7 @@ describe("collect_reward", () => {
rewardVault: rewards[0].rewardVaultKeypair.publicKey,
rewardIndex: 0,
})
)
.addSigner(delegate)
.buildAndExecute(),
/0x1783/ // MissingOrInvalidDelegate
@ -424,13 +511,15 @@ describe("collect_reward", () => {
});
it("fails when position authority is not authorized for exactly one token", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(1)),
initialSqrtPrice: MathUtil.toX64(new Decimal(1)),
positions: [
{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: new anchor.BN(1_000_000) },
],
rewards: [{ emissionsPerSecondX64: toX64(new Decimal(2)), vaultAmount: new u64(1_000_000) }],
rewards: [
{ emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)), vaultAmount: new u64(1_000_000) },
],
});
const {
poolInitInfo: { whirlpoolPda },
@ -445,8 +534,9 @@ describe("collect_reward", () => {
const delegate = anchor.web3.Keypair.generate();
await approveToken(provider, positions[0].tokenAccount, delegate.publicKey, 2);
await assert.rejects(
client
.collectRewardTx({
toTx(
ctx,
WhirlpoolIx.collectRewardIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: delegate.publicKey,
position: positions[0].publicKey,
@ -455,6 +545,7 @@ describe("collect_reward", () => {
rewardVault: rewards[0].rewardVaultKeypair.publicKey,
rewardIndex: 0,
})
)
.addSigner(delegate)
.buildAndExecute(),
/0x1784/ // InvalidPositionTokenAmount
@ -462,13 +553,15 @@ describe("collect_reward", () => {
});
it("fails when position authority was not a signer", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(1)),
initialSqrtPrice: MathUtil.toX64(new Decimal(1)),
positions: [
{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: new anchor.BN(1_000_000) },
],
rewards: [{ emissionsPerSecondX64: toX64(new Decimal(2)), vaultAmount: new u64(1_000_000) }],
rewards: [
{ emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)), vaultAmount: new u64(1_000_000) },
],
});
const {
poolInitInfo: { whirlpoolPda },
@ -483,8 +576,9 @@ describe("collect_reward", () => {
const delegate = anchor.web3.Keypair.generate();
await approveToken(provider, positions[0].tokenAccount, delegate.publicKey, 1);
await assert.rejects(
client
.collectRewardTx({
toTx(
ctx,
WhirlpoolIx.collectRewardIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: delegate.publicKey,
position: positions[0].publicKey,
@ -493,19 +587,21 @@ describe("collect_reward", () => {
rewardVault: rewards[0].rewardVaultKeypair.publicKey,
rewardIndex: 0,
})
.buildAndExecute(),
).buildAndExecute(),
/Signature verification failed/
);
});
it("fails when reward vault does not match whirlpool reward vault", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(1)),
initialSqrtPrice: MathUtil.toX64(new Decimal(1)),
positions: [
{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: new anchor.BN(1_000_000) },
],
rewards: [{ emissionsPerSecondX64: toX64(new Decimal(2)), vaultAmount: new u64(1_000_000) }],
rewards: [
{ emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)), vaultAmount: new u64(1_000_000) },
],
});
const {
poolInitInfo: { whirlpoolPda },
@ -518,8 +614,9 @@ describe("collect_reward", () => {
provider.wallet.publicKey
);
await assert.rejects(
client
.collectRewardTx({
toTx(
ctx,
WhirlpoolIx.collectRewardIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: provider.wallet.publicKey,
position: positions[0].publicKey,
@ -528,19 +625,21 @@ describe("collect_reward", () => {
rewardVault: rewardOwnerAccount,
rewardIndex: 0,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7dc/ // ConstraintAddress
);
});
it("fails when reward owner account mint does not match whirlpool reward mint", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(1)),
initialSqrtPrice: MathUtil.toX64(new Decimal(1)),
positions: [
{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: new anchor.BN(1_000_000) },
],
rewards: [{ emissionsPerSecondX64: toX64(new Decimal(2)), vaultAmount: new u64(1_000_000) }],
rewards: [
{ emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)), vaultAmount: new u64(1_000_000) },
],
});
const {
poolInitInfo: { whirlpoolPda, tokenMintA },
@ -553,8 +652,9 @@ describe("collect_reward", () => {
provider.wallet.publicKey
);
await assert.rejects(
client
.collectRewardTx({
toTx(
ctx,
WhirlpoolIx.collectRewardIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: provider.wallet.publicKey,
position: positions[0].publicKey,
@ -563,19 +663,21 @@ describe("collect_reward", () => {
rewardVault: rewards[0].rewardVaultKeypair.publicKey,
rewardIndex: 0,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
});
it("fails when reward index is out of bounds", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(1)),
initialSqrtPrice: MathUtil.toX64(new Decimal(1)),
positions: [
{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: new anchor.BN(1_000_000) },
],
rewards: [{ emissionsPerSecondX64: toX64(new Decimal(2)), vaultAmount: new u64(1_000_000) }],
rewards: [
{ emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)), vaultAmount: new u64(1_000_000) },
],
});
const {
poolInitInfo: { whirlpoolPda, tokenMintA },
@ -588,8 +690,9 @@ describe("collect_reward", () => {
provider.wallet.publicKey
);
await assert.rejects(
client
.collectRewardTx({
toTx(
ctx,
WhirlpoolIx.collectRewardIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
positionAuthority: provider.wallet.publicKey,
position: positions[0].publicKey,
@ -598,7 +701,7 @@ describe("collect_reward", () => {
rewardVault: rewards[0].rewardVaultKeypair.publicKey,
rewardIndex: 4,
})
.buildAndExecute(),
).buildAndExecute(),
/Program failed to complete/ // index out of bounds
);
});

View File

@ -1,47 +1,64 @@
import * as anchor from "@project-serum/anchor";
import { WhirlpoolClient } from "../src/client";
import { WhirlpoolContext } from "../src/context";
import * as assert from "assert";
import { initTestPool, initTickArray, openPosition } from "./utils/init-utils";
import { u64 } from "@solana/spl-token";
import Decimal from "decimal.js";
import {
WhirlpoolContext,
AccountFetcher,
WhirlpoolData,
TickArrayData,
PositionData,
WhirlpoolIx,
} from "../../src";
import {
TickSpacing,
assertTick,
approveToken,
createAndMintToTokenAccount,
createMint,
createTokenAccount,
transfer,
} from "./utils/token";
import { u64 } from "@solana/spl-token";
import { toX64 } from "../src";
import Decimal from "decimal.js";
import { assertTick, TickSpacing, ZERO_BN } from "./utils";
import { WhirlpoolTestFixture } from "./utils/fixture";
ZERO_BN,
createAndMintToTokenAccount,
createMint,
} from "../utils";
import { WhirlpoolTestFixture } from "../utils/fixture";
import { initTestPool, openPosition, initTickArray } from "../utils/init-utils";
import { decreaseLiquidityQuoteByLiquidityWithParams } from "../../src/quotes/public/decrease-liquidity-quote";
import { MathUtil, Percentage } from "@orca-so/common-sdk";
import { toTx } from "../../src/utils/instructions-util";
describe("decrease_liquidity", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
// TODO: Convert these test cases to take a non-zero min token amount to verify that the calculation is working.
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
it("successfully decrease liquidity from position in one tick array", async () => {
const liquidityAmount = new u64(1_250_000);
const fixture = await new WhirlpoolTestFixture(client).init({
const tickLower = 7168,
tickUpper = 8960;
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(1.48)),
positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }],
initialSqrtPrice: MathUtil.toX64(new Decimal(1.48)),
positions: [{ tickLowerIndex: tickLower, tickUpperIndex: tickUpper, liquidityAmount }],
});
const { poolInitInfo, tokenAccountA, tokenAccountB, positions } = fixture.getInfos();
const { whirlpoolPda, tokenVaultAKeypair, tokenVaultBKeypair } = poolInitInfo;
const poolBefore = await client.getPool(whirlpoolPda.publicKey);
const poolBefore = (await fetcher.getPool(whirlpoolPda.publicKey, true)) as WhirlpoolData;
const removeAmount = new u64(1_000_000);
await client
.decreaseLiquidityTx({
liquidityAmount: removeAmount,
tokenMinA: new u64(0),
tokenMinB: new u64(0),
const removalQuote = decreaseLiquidityQuoteByLiquidityWithParams({
liquidity: new u64(1_000_000),
sqrtPrice: poolBefore.sqrtPrice,
slippageTolerance: Percentage.fromFraction(1, 100),
tickCurrentIndex: poolBefore.tickCurrentIndex,
tickLowerIndex: tickLower,
tickUpperIndex: tickUpper,
});
await toTx(
ctx,
WhirlpoolIx.decreaseLiquidityIx(ctx.program, {
...removalQuote,
whirlpool: whirlpoolPda.publicKey,
positionAuthority: provider.wallet.publicKey,
position: positions[0].publicKey,
@ -53,39 +70,51 @@ describe("decrease_liquidity", () => {
tickArrayLower: positions[0].tickArrayLower,
tickArrayUpper: positions[0].tickArrayUpper,
})
.buildAndExecute();
).buildAndExecute();
const remainingLiquidity = liquidityAmount.sub(removeAmount);
const poolAfter = await client.getPool(whirlpoolPda.publicKey);
const remainingLiquidity = liquidityAmount.sub(removalQuote.liquidityAmount);
const poolAfter = (await fetcher.getPool(whirlpoolPda.publicKey, true)) as WhirlpoolData;
assert.ok(poolAfter.rewardLastUpdatedTimestamp.gte(poolBefore.rewardLastUpdatedTimestamp));
assert.ok(poolAfter.liquidity.eq(remainingLiquidity));
const position = await client.getPosition(positions[0].publicKey);
assert.ok(position.liquidity.eq(remainingLiquidity));
const position = await fetcher.getPosition(positions[0].publicKey, true);
assert.ok(position?.liquidity.eq(remainingLiquidity));
const tickArray = await client.getTickArray(positions[0].tickArrayLower);
const tickArray = (await fetcher.getTickArray(
positions[0].tickArrayLower,
true
)) as TickArrayData;
assertTick(tickArray.ticks[56], true, remainingLiquidity, remainingLiquidity);
assertTick(tickArray.ticks[70], true, remainingLiquidity, remainingLiquidity.neg());
});
it("successfully decrease liquidity from position in two tick arrays", async () => {
const liquidityAmount = new u64(1_250_000);
const fixture = await new WhirlpoolTestFixture(client).init({
const tickLower = -1280,
tickUpper = 1280;
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(1)),
initialSqrtPrice: MathUtil.toX64(new Decimal(1)),
positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount }],
});
const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos();
const { whirlpoolPda, tokenVaultAKeypair, tokenVaultBKeypair } = poolInitInfo;
const position = positions[0];
const poolBefore = await client.getPool(whirlpoolPda.publicKey);
const poolBefore = (await fetcher.getPool(whirlpoolPda.publicKey, true)) as WhirlpoolData;
const removeAmount = new u64(1_000_000);
await client
.decreaseLiquidityTx({
liquidityAmount: removeAmount,
tokenMinA: new u64(0),
tokenMinB: new u64(0),
const removalQuote = decreaseLiquidityQuoteByLiquidityWithParams({
liquidity: new u64(1_000_000),
sqrtPrice: poolBefore.sqrtPrice,
slippageTolerance: Percentage.fromFraction(1, 100),
tickCurrentIndex: poolBefore.tickCurrentIndex,
tickLowerIndex: tickLower,
tickUpperIndex: tickUpper,
});
await toTx(
ctx,
WhirlpoolIx.decreaseLiquidityIx(ctx.program, {
...removalQuote,
whirlpool: whirlpoolPda.publicKey,
positionAuthority: provider.wallet.publicKey,
position: position.publicKey,
@ -97,28 +126,34 @@ describe("decrease_liquidity", () => {
tickArrayLower: position.tickArrayLower,
tickArrayUpper: position.tickArrayUpper,
})
.buildAndExecute();
).buildAndExecute();
const remainingLiquidity = liquidityAmount.sub(removeAmount);
const poolAfter = await client.getPool(whirlpoolPda.publicKey);
const remainingLiquidity = liquidityAmount.sub(removalQuote.liquidityAmount);
const poolAfter = (await fetcher.getPool(whirlpoolPda.publicKey, true)) as WhirlpoolData;
assert.ok(poolAfter.rewardLastUpdatedTimestamp.gte(poolBefore.rewardLastUpdatedTimestamp));
assert.ok(poolAfter.liquidity.eq(remainingLiquidity));
const positionAfter = await client.getPosition(position.publicKey);
const positionAfter = (await fetcher.getPosition(position.publicKey, true)) as PositionData;
assert.ok(positionAfter.liquidity.eq(remainingLiquidity));
const tickArrayLower = await client.getTickArray(position.tickArrayLower);
const tickArrayLower = (await fetcher.getTickArray(
position.tickArrayLower,
true
)) as TickArrayData;
assertTick(tickArrayLower.ticks[78], true, remainingLiquidity, remainingLiquidity);
const tickArrayUpper = await client.getTickArray(position.tickArrayUpper);
const tickArrayUpper = (await fetcher.getTickArray(
position.tickArrayUpper,
true
)) as TickArrayData;
assertTick(tickArrayUpper.ticks[10], true, remainingLiquidity, remainingLiquidity.neg());
});
it("successfully decrease liquidity with approved delegate", async () => {
const liquidityAmount = new u64(1_250_000);
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(1)),
initialSqrtPrice: MathUtil.toX64(new Decimal(1)),
positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount }],
});
const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos();
@ -133,8 +168,9 @@ describe("decrease_liquidity", () => {
const removeAmount = new u64(1_000_000);
await client
.decreaseLiquidityTx({
await toTx(
ctx,
WhirlpoolIx.decreaseLiquidityIx(ctx.program, {
liquidityAmount: removeAmount,
tokenMinA: new u64(0),
tokenMinB: new u64(0),
@ -149,15 +185,16 @@ describe("decrease_liquidity", () => {
tickArrayLower: position.tickArrayLower,
tickArrayUpper: position.tickArrayUpper,
})
)
.addSigner(delegate)
.buildAndExecute();
});
it("successfully decrease liquidity with owner even if there is approved delegate", async () => {
const liquidityAmount = new u64(1_250_000);
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(1.48)),
initialSqrtPrice: MathUtil.toX64(new Decimal(1.48)),
positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }],
});
const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos();
@ -172,8 +209,9 @@ describe("decrease_liquidity", () => {
const removeAmount = new u64(1_000_000);
await client
.decreaseLiquidityTx({
await toTx(
ctx,
WhirlpoolIx.decreaseLiquidityIx(ctx.program, {
liquidityAmount: removeAmount,
tokenMinA: new u64(0),
tokenMinB: new u64(0),
@ -188,14 +226,14 @@ describe("decrease_liquidity", () => {
tickArrayLower: position.tickArrayLower,
tickArrayUpper: position.tickArrayUpper,
})
.buildAndExecute();
).buildAndExecute();
});
it("successfully decrease liquidity with transferred position token", async () => {
const liquidityAmount = new u64(1_250_000);
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(1.48)),
initialSqrtPrice: MathUtil.toX64(new Decimal(1.48)),
positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }],
});
const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos();
@ -211,8 +249,9 @@ describe("decrease_liquidity", () => {
);
await transfer(provider, position.tokenAccount, newOwnerPositionTokenAccount, 1);
await client
.decreaseLiquidityTx({
await toTx(
ctx,
WhirlpoolIx.decreaseLiquidityIx(ctx.program, {
liquidityAmount: removeAmount,
tokenMinA: new u64(0),
tokenMinB: new u64(0),
@ -227,15 +266,16 @@ describe("decrease_liquidity", () => {
tickArrayLower: position.tickArrayLower,
tickArrayUpper: position.tickArrayUpper,
})
)
.addSigner(newOwner)
.buildAndExecute();
});
it("fails when liquidity amount is zero", async () => {
const liquidityAmount = new u64(1_250_000);
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(1)),
initialSqrtPrice: MathUtil.toX64(new Decimal(1)),
positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount }],
});
const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos();
@ -243,8 +283,9 @@ describe("decrease_liquidity", () => {
const position = positions[0];
await assert.rejects(
client
.decreaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.decreaseLiquidityIx(ctx.program, {
liquidityAmount: new u64(0),
tokenMinA: new u64(0),
tokenMinB: new u64(0),
@ -259,15 +300,15 @@ describe("decrease_liquidity", () => {
tickArrayLower: position.tickArrayLower,
tickArrayUpper: position.tickArrayUpper,
})
.buildAndExecute(),
).buildAndExecute(),
/0x177c/ // LiquidityZero
);
});
it("fails when position has insufficient liquidity for the withdraw amount", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(1)),
initialSqrtPrice: MathUtil.toX64(new Decimal(1)),
positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: ZERO_BN }],
});
const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos();
@ -275,8 +316,9 @@ describe("decrease_liquidity", () => {
const position = positions[0];
await assert.rejects(
client
.decreaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.decreaseLiquidityIx(ctx.program, {
liquidityAmount: new u64(1_000),
tokenMinA: new u64(0),
tokenMinB: new u64(0),
@ -291,16 +333,16 @@ describe("decrease_liquidity", () => {
tickArrayLower: position.tickArrayLower,
tickArrayUpper: position.tickArrayUpper,
})
.buildAndExecute(),
).buildAndExecute(),
/0x177f/ // LiquidityUnderflow
);
});
it("fails when token min a subceeded", async () => {
const liquidityAmount = new u64(1_250_000);
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(0.005)),
initialSqrtPrice: MathUtil.toX64(new Decimal(0.005)),
positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount }],
});
const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos();
@ -308,8 +350,9 @@ describe("decrease_liquidity", () => {
const position = positions[0];
await assert.rejects(
client
.decreaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.decreaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMinA: new u64(1_000_000),
tokenMinB: new u64(0),
@ -324,24 +367,25 @@ describe("decrease_liquidity", () => {
tickArrayLower: position.tickArrayLower,
tickArrayUpper: position.tickArrayUpper,
})
.buildAndExecute(),
).buildAndExecute(),
/0x1782/ // TokenMinSubceeded
);
});
it("fails when token min b subceeded", async () => {
const liquidityAmount = new u64(1_250_000);
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(5)),
initialSqrtPrice: MathUtil.toX64(new Decimal(5)),
positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount }],
});
const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos();
const { whirlpoolPda, tokenVaultAKeypair, tokenVaultBKeypair } = poolInitInfo;
const position = positions[0];
await assert.rejects(
client
.decreaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.decreaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMinA: new u64(0),
tokenMinB: new u64(1_000_000),
@ -356,16 +400,16 @@ describe("decrease_liquidity", () => {
tickArrayLower: position.tickArrayLower,
tickArrayUpper: position.tickArrayUpper,
})
.buildAndExecute(),
).buildAndExecute(),
/0x1782/ // TokenMinSubceeded
);
});
it("fails when position account does not have exactly 1 token", async () => {
const liquidityAmount = new u64(1_250_000);
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(2.2)),
initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)),
positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }],
});
const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos();
@ -380,8 +424,9 @@ describe("decrease_liquidity", () => {
);
await assert.rejects(
client
.decreaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.decreaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMinA: new u64(0),
tokenMinB: new u64(0),
@ -396,7 +441,7 @@ describe("decrease_liquidity", () => {
tickArrayLower: position.tickArrayLower,
tickArrayUpper: position.tickArrayUpper,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
@ -404,8 +449,9 @@ describe("decrease_liquidity", () => {
await transfer(provider, position.tokenAccount, newPositionTokenAccount, 1);
await assert.rejects(
client
.decreaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.decreaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMinA: new u64(0),
tokenMinB: new u64(0),
@ -420,16 +466,16 @@ describe("decrease_liquidity", () => {
tickArrayLower: position.tickArrayLower,
tickArrayUpper: position.tickArrayUpper,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
});
it("fails when position token account mint does not match position mint", async () => {
const liquidityAmount = new u64(6_500_000);
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(2.2)),
initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)),
positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }],
});
const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos();
@ -439,8 +485,9 @@ describe("decrease_liquidity", () => {
const invalidPositionTokenAccount = await createAndMintToTokenAccount(provider, tokenMintA, 1);
await assert.rejects(
client
.decreaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.decreaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMinA: new u64(0),
tokenMinB: new u64(0),
@ -455,30 +502,31 @@ describe("decrease_liquidity", () => {
tickArrayLower: position.tickArrayLower,
tickArrayUpper: position.tickArrayUpper,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // A raw constraint was violated
);
});
it("fails when position does not match whirlpool", async () => {
const liquidityAmount = new u64(6_500_000);
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(2.2)),
initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)),
positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }],
});
const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos();
const { whirlpoolPda } = poolInitInfo;
const tickArray = positions[0].tickArrayLower;
const { poolInitInfo: poolInitInfo2 } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo: poolInitInfo2 } = await initTestPool(ctx, TickSpacing.Standard);
const {
params: { positionPda, positionTokenAccountAddress },
} = await openPosition(client, poolInitInfo2.whirlpoolPda.publicKey, 7168, 8960);
params: { positionPda, positionTokenAccount: positionTokenAccountAddress },
} = await openPosition(ctx, poolInitInfo2.whirlpoolPda.publicKey, 7168, 8960);
await assert.rejects(
client
.decreaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.decreaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMinA: new u64(0),
tokenMinB: new u64(0),
@ -493,16 +541,16 @@ describe("decrease_liquidity", () => {
tickArrayLower: tickArray,
tickArrayUpper: tickArray,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d1/ // A has_one constraint was violated
);
});
it("fails when token vaults do not match whirlpool vaults", async () => {
const liquidityAmount = new u64(6_500_000);
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(2.2)),
initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)),
positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }],
});
const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos();
@ -513,8 +561,9 @@ describe("decrease_liquidity", () => {
const fakeVaultB = await createAndMintToTokenAccount(provider, tokenMintB, 1_000);
await assert.rejects(
client
.decreaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.decreaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMinA: new u64(0),
tokenMinB: new u64(0),
@ -529,13 +578,14 @@ describe("decrease_liquidity", () => {
tickArrayLower: position.tickArrayLower,
tickArrayUpper: position.tickArrayUpper,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
await assert.rejects(
client
.decreaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.decreaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMinA: new u64(0),
tokenMinB: new u64(0),
@ -550,16 +600,16 @@ describe("decrease_liquidity", () => {
tickArrayLower: position.tickArrayLower,
tickArrayUpper: position.tickArrayUpper,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
});
it("fails when owner token account mint does not match whirlpool token mint", async () => {
const liquidityAmount = new u64(6_500_000);
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(2.2)),
initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)),
positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }],
});
const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos();
@ -569,8 +619,9 @@ describe("decrease_liquidity", () => {
const position = positions[0];
await assert.rejects(
client
.decreaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.decreaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMinA: new u64(0),
tokenMinB: new u64(0),
@ -585,13 +636,14 @@ describe("decrease_liquidity", () => {
tickArrayLower: position.tickArrayLower,
tickArrayUpper: position.tickArrayUpper,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
await assert.rejects(
client
.decreaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.decreaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMinA: new u64(0),
tokenMinB: new u64(0),
@ -606,16 +658,16 @@ describe("decrease_liquidity", () => {
tickArrayLower: position.tickArrayLower,
tickArrayUpper: position.tickArrayUpper,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
});
it("fails when position authority is not approved delegate for position token account", async () => {
const liquidityAmount = new u64(6_500_000);
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(2.2)),
initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)),
positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }],
});
const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos();
@ -627,8 +679,9 @@ describe("decrease_liquidity", () => {
await approveToken(provider, tokenAccountB, delegate.publicKey, 1_000_000);
await assert.rejects(
client
.decreaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.decreaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMinA: new u64(0),
tokenMinB: new u64(0),
@ -643,6 +696,7 @@ describe("decrease_liquidity", () => {
tickArrayLower: position.tickArrayLower,
tickArrayUpper: position.tickArrayUpper,
})
)
.addSigner(delegate)
.buildAndExecute(),
/0x1783/ // MissingOrInvalidDelegate
@ -651,9 +705,9 @@ describe("decrease_liquidity", () => {
it("fails when position authority is not authorized for exactly 1 token", async () => {
const liquidityAmount = new u64(6_500_000);
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(2.2)),
initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)),
positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }],
});
const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos();
@ -666,8 +720,9 @@ describe("decrease_liquidity", () => {
await approveToken(provider, tokenAccountB, delegate.publicKey, 1_000_000);
await assert.rejects(
client
.decreaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.decreaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMinA: new u64(0),
tokenMinB: new u64(0),
@ -682,6 +737,7 @@ describe("decrease_liquidity", () => {
tickArrayLower: position.tickArrayLower,
tickArrayUpper: position.tickArrayUpper,
})
)
.addSigner(delegate)
.buildAndExecute(),
/0x1784/ // InvalidPositionTokenAmount
@ -690,9 +746,9 @@ describe("decrease_liquidity", () => {
it("fails when position authority was not a signer", async () => {
const liquidityAmount = new u64(6_500_000);
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(2.2)),
initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)),
positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }],
});
const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos();
@ -705,8 +761,9 @@ describe("decrease_liquidity", () => {
await approveToken(provider, tokenAccountB, delegate.publicKey, 1_000_000);
await assert.rejects(
client
.decreaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.decreaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMinA: new u64(0),
tokenMinB: new u64(167_000),
@ -721,16 +778,16 @@ describe("decrease_liquidity", () => {
tickArrayLower: position.tickArrayLower,
tickArrayUpper: position.tickArrayUpper,
})
.buildAndExecute(),
).buildAndExecute(),
/Signature verification failed/
);
});
it("fails when tick arrays do not match the position", async () => {
const liquidityAmount = new u64(6_500_000);
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(2.2)),
initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)),
positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }],
});
const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos();
@ -739,15 +796,16 @@ describe("decrease_liquidity", () => {
const {
params: { tickArrayPda: tickArrayLowerPda },
} = await initTickArray(client, whirlpoolPda.publicKey, 11264);
} = await initTickArray(ctx, whirlpoolPda.publicKey, 11264);
const {
params: { tickArrayPda: tickArrayUpperPda },
} = await initTickArray(client, whirlpoolPda.publicKey, 22528);
} = await initTickArray(ctx, whirlpoolPda.publicKey, 22528);
await assert.rejects(
client
.decreaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.decreaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMinA: new u64(0),
tokenMinB: new u64(0),
@ -762,35 +820,36 @@ describe("decrease_liquidity", () => {
tickArrayLower: tickArrayLowerPda.publicKey,
tickArrayUpper: tickArrayUpperPda.publicKey,
})
.buildAndExecute(),
).buildAndExecute(),
/0x1779/ // TicKNotFound
);
});
it("fails when the tick arrays are for a different whirlpool", async () => {
const liquidityAmount = new u64(6_500_000);
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(2.2)),
initialSqrtPrice: MathUtil.toX64(new Decimal(2.2)),
positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount }],
});
const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos();
const { whirlpoolPda } = poolInitInfo;
const position = positions[0];
const { poolInitInfo: poolInitInfo2 } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo: poolInitInfo2 } = await initTestPool(ctx, TickSpacing.Standard);
const {
params: { tickArrayPda: tickArrayLowerPda },
} = await initTickArray(client, poolInitInfo2.whirlpoolPda.publicKey, -11264);
} = await initTickArray(ctx, poolInitInfo2.whirlpoolPda.publicKey, -11264);
const {
params: { tickArrayPda: tickArrayUpperPda },
} = await initTickArray(client, poolInitInfo2.whirlpoolPda.publicKey, 0);
} = await initTickArray(ctx, poolInitInfo2.whirlpoolPda.publicKey, 0);
await assert.rejects(
client
.decreaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.decreaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMinA: new u64(0),
tokenMinB: new u64(0),
@ -805,7 +864,7 @@ describe("decrease_liquidity", () => {
tickArrayLower: tickArrayLowerPda.publicKey,
tickArrayUpper: tickArrayUpperPda.publicKey,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d1/ // A has one constraint was violated
);
});

View File

@ -1,72 +1,72 @@
import * as anchor from "@project-serum/anchor";
import { WhirlpoolClient } from "../src/client";
import { WhirlpoolContext } from "../src/context";
import * as assert from "assert";
import { initTestPool, initTickArray, openPosition } from "./utils/init-utils";
import * as anchor from "@project-serum/anchor";
import { u64 } from "@solana/spl-token";
import Decimal from "decimal.js";
import {
WhirlpoolContext,
AccountFetcher,
WhirlpoolData,
PositionData,
TickArrayData,
TickUtil,
PriceMath,
WhirlpoolIx,
PDAUtil,
} from "../../src";
import {
TickSpacing,
ZERO_BN,
getTokenBalance,
assertTick,
approveToken,
createAndMintToTokenAccount,
createMint,
MAX_U64,
createTokenAccount,
getTokenBalance,
transfer,
} from "./utils/token";
createMint,
} from "../utils";
import { WhirlpoolTestFixture } from "../utils/fixture";
import { initTestPool, initTickArray, openPosition } from "../utils/init-utils";
import {
estimateLiquidityFromTokenAmounts,
getStartTickIndex,
getTickArrayPda,
tickIndexToSqrtPriceX64,
toTokenAmount,
toX64,
TransactionBuilder,
} from "../src";
import Decimal from "decimal.js";
import { u64 } from "@solana/spl-token";
import { assertTick, MAX_U64, TickSpacing, ZERO_BN } from "./utils";
import { WhirlpoolTestFixture } from "./utils/fixture";
import { BN } from "@project-serum/anchor";
import { buildIncreaseLiquidityIx } from "../src/instructions/increase-liquidity-ix";
import {
buildOpenPositionIx,
buildOpenPositionWithMetadataIx,
} from "../src/instructions/open-position-ix";
import {
generateDefaultInitTickArrayParams,
generateDefaultOpenPositionParams,
} from "./utils/test-builders";
import { buildInitTickArrayIx } from "../src/instructions/initialize-tick-array-ix";
generateDefaultInitTickArrayParams,
} from "../utils/test-builders";
import { PoolUtil, toTokenAmount } from "../../src/utils/public/pool-utils";
import { MathUtil, TransactionBuilder } from "@orca-so/common-sdk";
import { toTx } from "../../src/utils/instructions-util";
describe("increase_liquidity", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
it("increase liquidity of a position spanning two tick arrays", async () => {
const currTick = 0;
const tickLowerIndex = -1280,
tickUpperIndex = 1280;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: ZERO_BN }],
initialSqrtPrice: tickIndexToSqrtPriceX64(currTick),
initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currTick),
});
const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos();
const { whirlpoolPda } = poolInitInfo;
const positionInitInfo = positions[0];
const poolBefore = await client.getPool(whirlpoolPda.publicKey);
const poolBefore = (await fetcher.getPool(whirlpoolPda.publicKey, true)) as WhirlpoolData;
const tokenAmount = toTokenAmount(167_000, 167_000);
const liquidityAmount = estimateLiquidityFromTokenAmounts(
const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts(
currTick,
tickLowerIndex,
tickUpperIndex,
tokenAmount
);
await client
.increaseLiquidityTx({
await toTx(
ctx,
WhirlpoolIx.increaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMaxA: tokenAmount.tokenA,
tokenMaxB: tokenAmount.tokenB,
@ -81,12 +81,12 @@ describe("increase_liquidity", () => {
tickArrayLower: positionInitInfo.tickArrayLower,
tickArrayUpper: positionInitInfo.tickArrayUpper,
})
.buildAndExecute();
).buildAndExecute();
const position = await client.getPosition(positionInitInfo.publicKey);
const position = (await fetcher.getPosition(positionInitInfo.publicKey, true)) as PositionData;
assert.ok(position.liquidity.eq(liquidityAmount));
const poolAfter = await client.getPool(whirlpoolPda.publicKey);
const poolAfter = (await fetcher.getPool(whirlpoolPda.publicKey, true)) as WhirlpoolData;
assert.ok(poolAfter.rewardLastUpdatedTimestamp.gte(poolBefore.rewardLastUpdatedTimestamp));
assert.equal(
await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey),
@ -96,11 +96,17 @@ describe("increase_liquidity", () => {
await getTokenBalance(provider, poolInitInfo.tokenVaultBKeypair.publicKey),
tokenAmount.tokenB.toString()
);
assert.ok(poolAfter.liquidity.eq(new BN(liquidityAmount)));
assert.ok(poolAfter.liquidity.eq(new anchor.BN(liquidityAmount)));
const tickArrayLower = await client.getTickArray(positionInitInfo.tickArrayLower);
const tickArrayLower = (await fetcher.getTickArray(
positionInitInfo.tickArrayLower,
true
)) as TickArrayData;
assertTick(tickArrayLower.ticks[78], true, liquidityAmount, liquidityAmount);
const tickArrayUpper = await client.getTickArray(positionInitInfo.tickArrayUpper);
const tickArrayUpper = (await fetcher.getTickArray(
positionInitInfo.tickArrayUpper,
true
)) as TickArrayData;
assertTick(tickArrayUpper.ticks[10], true, liquidityAmount, liquidityAmount.neg());
});
@ -108,26 +114,27 @@ describe("increase_liquidity", () => {
const currTick = 500;
const tickLowerIndex = 7168;
const tickUpperIndex = 8960;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: ZERO_BN }],
initialSqrtPrice: tickIndexToSqrtPriceX64(currTick),
initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currTick),
});
const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos();
const { whirlpoolPda } = poolInitInfo;
const positionInitInfo = positions[0];
const poolBefore = await client.getPool(whirlpoolPda.publicKey);
const poolBefore = (await fetcher.getPool(whirlpoolPda.publicKey, true)) as WhirlpoolData;
const tokenAmount = toTokenAmount(1_000_000, 0);
const liquidityAmount = estimateLiquidityFromTokenAmounts(
const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts(
currTick,
tickLowerIndex,
tickUpperIndex,
tokenAmount
);
await client
.increaseLiquidityTx({
await toTx(
ctx,
WhirlpoolIx.increaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMaxA: tokenAmount.tokenA,
tokenMaxB: tokenAmount.tokenB,
@ -142,7 +149,7 @@ describe("increase_liquidity", () => {
tickArrayLower: positionInitInfo.tickArrayLower,
tickArrayUpper: positionInitInfo.tickArrayUpper,
})
.buildAndExecute();
).buildAndExecute();
assert.equal(
await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey),
@ -155,15 +162,18 @@ describe("increase_liquidity", () => {
);
const expectedLiquidity = new anchor.BN(liquidityAmount);
const position = await client.getPosition(positionInitInfo.publicKey);
const position = (await fetcher.getPosition(positionInitInfo.publicKey, true)) as PositionData;
assert.ok(position.liquidity.eq(expectedLiquidity));
const tickArray = await client.getTickArray(positionInitInfo.tickArrayLower);
const tickArray = (await fetcher.getTickArray(
positionInitInfo.tickArrayLower,
true
)) as TickArrayData;
assertTick(tickArray.ticks[56], true, expectedLiquidity, expectedLiquidity);
assertTick(tickArray.ticks[70], true, expectedLiquidity, expectedLiquidity.neg());
const poolAfter = await client.getPool(whirlpoolPda.publicKey);
const poolAfter = (await fetcher.getPool(whirlpoolPda.publicKey, true)) as WhirlpoolData;
assert.ok(poolAfter.rewardLastUpdatedTimestamp.gte(poolBefore.rewardLastUpdatedTimestamp));
assert.equal(poolAfter.liquidity, 0);
});
@ -172,16 +182,16 @@ describe("increase_liquidity", () => {
const currTick = 500;
const tickLowerIndex = 7168;
const tickUpperIndex = 8960;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: tickIndexToSqrtPriceX64(currTick),
initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currTick),
});
const { poolInitInfo, tokenAccountA, tokenAccountB } = fixture.getInfos();
const { whirlpoolPda, tickSpacing } = poolInitInfo;
const poolBefore = await client.getPool(whirlpoolPda.publicKey);
const poolBefore = (await fetcher.getPool(whirlpoolPda.publicKey, true)) as WhirlpoolData;
const tokenAmount = toTokenAmount(1_000_000, 0);
const liquidityAmount = estimateLiquidityFromTokenAmounts(
const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts(
currTick,
tickLowerIndex,
tickUpperIndex,
@ -189,58 +199,58 @@ describe("increase_liquidity", () => {
);
const { params, mint } = await generateDefaultOpenPositionParams(
client.context,
ctx,
whirlpoolPda.publicKey,
tickLowerIndex,
tickUpperIndex,
context.wallet.publicKey
ctx.wallet.publicKey
);
const tickArrayLower = getTickArrayPda(
client.context.program.programId,
const tickArrayLower = PDAUtil.getTickArray(
ctx.program.programId,
whirlpoolPda.publicKey,
getStartTickIndex(tickLowerIndex, tickSpacing)
TickUtil.getStartTickIndex(tickLowerIndex, tickSpacing)
).publicKey;
const tickArrayUpper = getTickArrayPda(
client.context.program.programId,
const tickArrayUpper = PDAUtil.getTickArray(
ctx.program.programId,
whirlpoolPda.publicKey,
getStartTickIndex(tickUpperIndex, tickSpacing)
TickUtil.getStartTickIndex(tickUpperIndex, tickSpacing)
).publicKey;
await new TransactionBuilder(client.context.provider)
await new TransactionBuilder(ctx.provider)
// TODO: create a ComputeBudgetInstruction to request more compute
.addInstruction(
buildInitTickArrayIx(
client.context,
WhirlpoolIx.initTickArrayIx(
ctx.program,
generateDefaultInitTickArrayParams(
client.context,
ctx,
whirlpoolPda.publicKey,
getStartTickIndex(tickLowerIndex, tickSpacing)
TickUtil.getStartTickIndex(tickLowerIndex, tickSpacing)
)
)
)
// .addInstruction(
// buildInitTickArrayIx(client.context, generateDefaultInitTickArrayParams(
// client.context,
// buildtoTx(ctx, WhirlpoolIx.initTickArrayIx(generateDefaultInitTickArrayParams(
// ctx,
// whirlpoolPda.publicKey,
// getStartTickIndex(pos[0].tickLowerIndex + TICK_ARRAY_SIZE * tickSpacing, tickSpacing),
// ))
// )
.addInstruction(buildOpenPositionIx(client.context, params))
.addInstruction(WhirlpoolIx.openPositionIx(ctx.program, params))
// .addInstruction(
// buildOpenPositionWithMetadataIx(client.context, params)
// buildWhirlpoolIx.openPositionWithMetadataIx(ctx.program, params)
// )
.addSigner(mint)
.addInstruction(
buildIncreaseLiquidityIx(client.context, {
WhirlpoolIx.increaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMaxA: tokenAmount.tokenA,
tokenMaxB: tokenAmount.tokenB,
whirlpool: whirlpoolPda.publicKey,
positionAuthority: provider.wallet.publicKey,
position: params.positionPda.publicKey,
positionTokenAccount: params.positionTokenAccountAddress,
positionTokenAccount: params.positionTokenAccount,
tokenOwnerAccountA: tokenAccountA,
tokenOwnerAccountB: tokenAccountB,
tokenVaultA: poolInitInfo.tokenVaultAKeypair.publicKey,
@ -262,15 +272,18 @@ describe("increase_liquidity", () => {
);
const expectedLiquidity = new anchor.BN(liquidityAmount);
const position = await client.getPosition(params.positionPda.publicKey);
const position = (await fetcher.getPosition(
params.positionPda.publicKey,
true
)) as PositionData;
assert.ok(position.liquidity.eq(expectedLiquidity));
const tickArray = await client.getTickArray(tickArrayLower);
const tickArray = (await fetcher.getTickArray(tickArrayLower, true)) as TickArrayData;
assertTick(tickArray.ticks[56], true, expectedLiquidity, expectedLiquidity);
assertTick(tickArray.ticks[70], true, expectedLiquidity, expectedLiquidity.neg());
const poolAfter = await client.getPool(whirlpoolPda.publicKey);
const poolAfter = (await fetcher.getPool(whirlpoolPda.publicKey, true)) as WhirlpoolData;
assert.ok(poolAfter.rewardLastUpdatedTimestamp.gte(poolBefore.rewardLastUpdatedTimestamp));
assert.equal(poolAfter.liquidity, 0);
});
@ -279,18 +292,18 @@ describe("increase_liquidity", () => {
const currTick = 1300;
const tickLowerIndex = -1280,
tickUpperIndex = 1280;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: ZERO_BN }],
initialSqrtPrice: tickIndexToSqrtPriceX64(currTick),
initialSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currTick),
});
const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos();
const { whirlpoolPda } = poolInitInfo;
const positionInitInfo = positions[0];
const poolBefore = await client.getPool(whirlpoolPda.publicKey);
const poolBefore = (await fetcher.getPool(whirlpoolPda.publicKey, true)) as WhirlpoolData;
const tokenAmount = toTokenAmount(0, 167_000);
const liquidityAmount = estimateLiquidityFromTokenAmounts(
const liquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts(
currTick,
tickLowerIndex,
tickUpperIndex,
@ -302,8 +315,9 @@ describe("increase_liquidity", () => {
await approveToken(provider, tokenAccountA, delegate.publicKey, 1_000_000);
await approveToken(provider, tokenAccountB, delegate.publicKey, 1_000_000);
await client
.increaseLiquidityTx({
await toTx(
ctx,
WhirlpoolIx.increaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMaxA: tokenAmount.tokenA,
tokenMaxB: tokenAmount.tokenB,
@ -318,13 +332,14 @@ describe("increase_liquidity", () => {
tickArrayLower: positionInitInfo.tickArrayLower,
tickArrayUpper: positionInitInfo.tickArrayUpper,
})
)
.addSigner(delegate)
.buildAndExecute();
const position = await client.getPosition(positionInitInfo.publicKey);
const position = (await fetcher.getPosition(positionInitInfo.publicKey, true)) as PositionData;
assert.ok(position.liquidity.eq(liquidityAmount));
const poolAfter = await client.getPool(whirlpoolPda.publicKey);
const poolAfter = (await fetcher.getPool(whirlpoolPda.publicKey, true)) as WhirlpoolData;
assert.ok(poolAfter.rewardLastUpdatedTimestamp.gte(poolBefore.rewardLastUpdatedTimestamp));
assert.equal(
await getTokenBalance(provider, poolInitInfo.tokenVaultAKeypair.publicKey),
@ -336,18 +351,24 @@ describe("increase_liquidity", () => {
);
assert.equal(poolAfter.liquidity, 0);
const tickArrayLower = await client.getTickArray(positionInitInfo.tickArrayLower);
const tickArrayLower = (await fetcher.getTickArray(
positionInitInfo.tickArrayLower,
true
)) as TickArrayData;
assertTick(tickArrayLower.ticks[78], true, liquidityAmount, liquidityAmount);
const tickArrayUpper = await client.getTickArray(positionInitInfo.tickArrayUpper);
const tickArrayUpper = (await fetcher.getTickArray(
positionInitInfo.tickArrayUpper,
true
)) as TickArrayData;
assertTick(tickArrayUpper.ticks[10], true, liquidityAmount, liquidityAmount.neg());
});
it("add maximum amount of liquidity near minimum price", async () => {
const currTick = -443621;
const { poolInitInfo } = await initTestPool(
client,
ctx,
TickSpacing.Stable,
tickIndexToSqrtPriceX64(currTick)
PriceMath.tickIndexToSqrtPriceX64(currTick)
);
const { tokenMintA, tokenMintB, whirlpoolPda } = poolInitInfo;
@ -356,31 +377,32 @@ describe("increase_liquidity", () => {
const {
params: { tickArrayPda },
} = await initTickArray(client, whirlpoolPda.publicKey, -444224);
} = await initTickArray(ctx, whirlpoolPda.publicKey, -444224);
const tickLowerIndex = -443632;
const tickUpperIndex = -443624;
const positionInfo = await openPosition(
client,
ctx,
whirlpoolPda.publicKey,
tickLowerIndex,
tickUpperIndex
);
const { positionPda, positionTokenAccountAddress } = positionInfo.params;
const { positionPda, positionTokenAccount: positionTokenAccountAddress } = positionInfo.params;
const tokenAmount = {
tokenA: new u64(0),
tokenB: MAX_U64,
};
const estLiquidityAmount = estimateLiquidityFromTokenAmounts(
const estLiquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts(
currTick,
tickLowerIndex,
tickUpperIndex,
tokenAmount
);
await client
.increaseLiquidityTx({
await toTx(
ctx,
WhirlpoolIx.increaseLiquidityIx(ctx.program, {
liquidityAmount: estLiquidityAmount,
tokenMaxA: tokenAmount.tokenA,
tokenMaxB: tokenAmount.tokenB,
@ -395,18 +417,18 @@ describe("increase_liquidity", () => {
tickArrayLower: tickArrayPda.publicKey,
tickArrayUpper: tickArrayPda.publicKey,
})
.buildAndExecute();
).buildAndExecute();
const position = await client.getPosition(positionPda.publicKey);
const position = (await fetcher.getPosition(positionPda.publicKey, true)) as PositionData;
assert.ok(position.liquidity.eq(estLiquidityAmount));
});
it("add maximum amount of liquidity near maximum price", async () => {
const currTick = 443635;
const { poolInitInfo } = await initTestPool(
client,
ctx,
TickSpacing.Stable,
tickIndexToSqrtPriceX64(currTick)
PriceMath.tickIndexToSqrtPriceX64(currTick)
);
const { tokenMintA, tokenMintB, whirlpoolPda } = poolInitInfo;
@ -415,31 +437,32 @@ describe("increase_liquidity", () => {
const {
params: { tickArrayPda },
} = await initTickArray(client, whirlpoolPda.publicKey, 436480);
} = await initTickArray(ctx, whirlpoolPda.publicKey, 436480);
const tickLowerIndex = 436488;
const tickUpperIndex = 436496;
const positionInfo = await openPosition(
client,
ctx,
whirlpoolPda.publicKey,
tickLowerIndex,
tickUpperIndex
);
const { positionPda, positionTokenAccountAddress } = positionInfo.params;
const { positionPda, positionTokenAccount: positionTokenAccountAddress } = positionInfo.params;
const tokenAmount = {
tokenA: new u64(0),
tokenB: MAX_U64,
};
const estLiquidityAmount = estimateLiquidityFromTokenAmounts(
const estLiquidityAmount = PoolUtil.estimateLiquidityFromTokenAmounts(
currTick,
tickLowerIndex,
tickUpperIndex,
tokenAmount
);
await client
.increaseLiquidityTx({
await toTx(
ctx,
WhirlpoolIx.increaseLiquidityIx(ctx.program, {
liquidityAmount: estLiquidityAmount,
tokenMaxA: tokenAmount.tokenA,
tokenMaxB: tokenAmount.tokenB,
@ -454,14 +477,14 @@ describe("increase_liquidity", () => {
tickArrayLower: tickArrayPda.publicKey,
tickArrayUpper: tickArrayPda.publicKey,
})
.buildAndExecute();
).buildAndExecute();
const position = await client.getPosition(positionPda.publicKey);
const position = (await fetcher.getPosition(positionPda.publicKey, true)) as PositionData;
assert.ok(position.liquidity.eq(estLiquidityAmount));
});
it("fails with zero liquidity amount", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount: ZERO_BN }],
});
@ -470,8 +493,9 @@ describe("increase_liquidity", () => {
const positionInitInfo = positions[0];
await assert.rejects(
client
.increaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.increaseLiquidityIx(ctx.program, {
liquidityAmount: ZERO_BN,
tokenMaxA: new u64(0),
tokenMaxB: new u64(1_000_000),
@ -486,15 +510,15 @@ describe("increase_liquidity", () => {
tickArrayLower: positionInitInfo.tickArrayLower,
tickArrayUpper: positionInitInfo.tickArrayUpper,
})
.buildAndExecute(),
).buildAndExecute(),
/0x177c/ // LiquidityZero
);
});
it("fails when token max a exceeded", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
initialSqrtPrice: toX64(new Decimal(1)),
initialSqrtPrice: MathUtil.toX64(new Decimal(1)),
positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount: ZERO_BN }],
});
const { poolInitInfo, positions, tokenAccountA, tokenAccountB } = fixture.getInfos();
@ -504,8 +528,9 @@ describe("increase_liquidity", () => {
const liquidityAmount = new u64(6_500_000);
await assert.rejects(
client
.increaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.increaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMaxA: new u64(0),
tokenMaxB: new u64(999_999_999),
@ -520,13 +545,13 @@ describe("increase_liquidity", () => {
tickArrayLower: positionInitInfo.tickArrayLower,
tickArrayUpper: positionInitInfo.tickArrayUpper,
})
.buildAndExecute(),
).buildAndExecute(),
/0x1781/ // TokenMaxExceeded
);
});
it("fails when token max b exceeded", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount: ZERO_BN }],
});
@ -537,8 +562,9 @@ describe("increase_liquidity", () => {
const liquidityAmount = new u64(6_500_000);
await assert.rejects(
client
.increaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.increaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMaxA: new u64(999_999_999),
tokenMaxB: new u64(0),
@ -553,13 +579,13 @@ describe("increase_liquidity", () => {
tickArrayLower: positionInitInfo.tickArrayLower,
tickArrayUpper: positionInitInfo.tickArrayUpper,
})
.buildAndExecute(),
).buildAndExecute(),
/0x1781/ // TokenMaxExceeded
);
});
it("fails when position account does not have exactly 1 token", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount: ZERO_BN }],
});
@ -577,8 +603,9 @@ describe("increase_liquidity", () => {
const liquidityAmount = new u64(6_500_000);
await assert.rejects(
client
.increaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.increaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMaxA: new u64(0),
tokenMaxB: new u64(1_000_000),
@ -593,7 +620,7 @@ describe("increase_liquidity", () => {
tickArrayLower: positionInitInfo.tickArrayLower,
tickArrayUpper: positionInitInfo.tickArrayUpper,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
@ -601,8 +628,9 @@ describe("increase_liquidity", () => {
await transfer(provider, positionInitInfo.tokenAccount, newPositionTokenAccount, 1);
await assert.rejects(
client
.increaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.increaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMaxA: new u64(0),
tokenMaxB: new u64(1_000_000),
@ -617,13 +645,13 @@ describe("increase_liquidity", () => {
tickArrayLower: positionInitInfo.tickArrayLower,
tickArrayUpper: positionInitInfo.tickArrayUpper,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
});
it("fails when position token account mint does not match position mint", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount: ZERO_BN }],
});
@ -637,8 +665,9 @@ describe("increase_liquidity", () => {
const liquidityAmount = new u64(6_500_000);
await assert.rejects(
client
.increaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.increaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMaxA: new u64(0),
tokenMaxB: new u64(1_000_000),
@ -653,7 +682,7 @@ describe("increase_liquidity", () => {
tickArrayLower: positionInitInfo.tickArrayLower,
tickArrayUpper: positionInitInfo.tickArrayUpper,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // A raw constraint was violated
);
});
@ -661,31 +690,33 @@ describe("increase_liquidity", () => {
it("fails when position does not match whirlpool", async () => {
const tickLowerIndex = 7168;
const tickUpperIndex = 8960;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: ZERO_BN }],
});
const { poolInitInfo, tokenAccountA, tokenAccountB } = fixture.getInfos();
const { whirlpoolPda } = poolInitInfo;
const { poolInitInfo: poolInitInfo2 } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo: poolInitInfo2 } = await initTestPool(ctx, TickSpacing.Standard);
const positionInitInfo = await openPosition(
client,
ctx,
poolInitInfo2.whirlpoolPda.publicKey,
tickLowerIndex,
tickUpperIndex
);
const { positionPda, positionTokenAccountAddress } = positionInitInfo.params;
const { positionPda, positionTokenAccount: positionTokenAccountAddress } =
positionInitInfo.params;
const {
params: { tickArrayPda },
} = await initTickArray(client, poolInitInfo2.whirlpoolPda.publicKey, 0);
} = await initTickArray(ctx, poolInitInfo2.whirlpoolPda.publicKey, 0);
const liquidityAmount = new u64(6_500_000);
await assert.rejects(
client
.increaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.increaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMaxA: new u64(0),
tokenMaxB: new u64(1_000_000),
@ -700,13 +731,13 @@ describe("increase_liquidity", () => {
tickArrayLower: tickArrayPda.publicKey,
tickArrayUpper: tickArrayPda.publicKey,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d1/ // A has_one constraint was violated
);
});
it("fails when token vaults do not match whirlpool vaults", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount: ZERO_BN }],
});
@ -719,8 +750,9 @@ describe("increase_liquidity", () => {
const fakeVaultB = await createAndMintToTokenAccount(provider, tokenMintB, 1_000);
await assert.rejects(
client
.increaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.increaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMaxA: new u64(0),
tokenMaxB: new u64(1_000_000),
@ -735,13 +767,14 @@ describe("increase_liquidity", () => {
tickArrayLower: positionInitInfo.tickArrayLower,
tickArrayUpper: positionInitInfo.tickArrayUpper,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
await assert.rejects(
client
.increaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.increaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMaxA: new u64(0),
tokenMaxB: new u64(1_000_000),
@ -756,13 +789,13 @@ describe("increase_liquidity", () => {
tickArrayLower: positionInitInfo.tickArrayLower,
tickArrayUpper: positionInitInfo.tickArrayUpper,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
});
it("fails when owner token account mint does not match whirlpool token mint", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
positions: [{ tickLowerIndex: 7168, tickUpperIndex: 8960, liquidityAmount: ZERO_BN }],
});
@ -775,8 +808,9 @@ describe("increase_liquidity", () => {
const invalidTokenAccount = await createAndMintToTokenAccount(provider, invalidMint, 1_000_000);
await assert.rejects(
client
.increaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.increaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMaxA: new u64(0),
tokenMaxB: new u64(1_000_000),
@ -791,13 +825,14 @@ describe("increase_liquidity", () => {
tickArrayLower: positionInitInfo.tickArrayLower,
tickArrayUpper: positionInitInfo.tickArrayUpper,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
await assert.rejects(
client
.increaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.increaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMaxA: new u64(0),
tokenMaxB: new u64(1_000_000),
@ -812,13 +847,13 @@ describe("increase_liquidity", () => {
tickArrayLower: positionInitInfo.tickArrayLower,
tickArrayUpper: positionInitInfo.tickArrayUpper,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
});
it("fails when position authority is not approved delegate for position token account", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: ZERO_BN }],
});
@ -834,8 +869,9 @@ describe("increase_liquidity", () => {
await approveToken(provider, tokenAccountB, delegate.publicKey, 1_000_000);
await assert.rejects(
client
.increaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.increaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMaxA: new u64(0),
tokenMaxB: new u64(167_000),
@ -850,6 +886,7 @@ describe("increase_liquidity", () => {
tickArrayLower: positionInitInfo.tickArrayLower,
tickArrayUpper: positionInitInfo.tickArrayUpper,
})
)
.addSigner(delegate)
.buildAndExecute(),
/0x1783/ // MissingOrInvalidDelegate
@ -857,7 +894,7 @@ describe("increase_liquidity", () => {
});
it("fails when position authority is not authorized for exactly 1 token", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: ZERO_BN }],
});
@ -874,8 +911,9 @@ describe("increase_liquidity", () => {
await approveToken(provider, tokenAccountB, delegate.publicKey, 1_000_000);
await assert.rejects(
client
.increaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.increaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMaxA: new u64(0),
tokenMaxB: new u64(167_000),
@ -890,6 +928,7 @@ describe("increase_liquidity", () => {
tickArrayLower: positionInitInfo.tickArrayLower,
tickArrayUpper: positionInitInfo.tickArrayUpper,
})
)
.addSigner(delegate)
.buildAndExecute(),
/0x1784/ // InvalidPositionTokenAmount
@ -897,7 +936,7 @@ describe("increase_liquidity", () => {
});
it("fails when position authority was not a signer", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: ZERO_BN }],
});
@ -914,8 +953,9 @@ describe("increase_liquidity", () => {
await approveToken(provider, tokenAccountB, delegate.publicKey, 1_000_000);
await assert.rejects(
client
.increaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.increaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMaxA: new u64(0),
tokenMaxB: new u64(167_000),
@ -930,13 +970,13 @@ describe("increase_liquidity", () => {
tickArrayLower: positionInitInfo.tickArrayLower,
tickArrayUpper: positionInitInfo.tickArrayUpper,
})
.buildAndExecute(),
).buildAndExecute(),
/Signature verification failed/
);
});
it("fails when position authority is not approved for token owner accounts", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: ZERO_BN }],
});
@ -951,8 +991,9 @@ describe("increase_liquidity", () => {
await approveToken(provider, positionInitInfo.tokenAccount, delegate.publicKey, 1);
await assert.rejects(
client
.increaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.increaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMaxA: new u64(0),
tokenMaxB: new u64(167_000),
@ -967,6 +1008,7 @@ describe("increase_liquidity", () => {
tickArrayLower: positionInitInfo.tickArrayLower,
tickArrayUpper: positionInitInfo.tickArrayUpper,
})
)
.addSigner(delegate)
.buildAndExecute(),
/0x4/ // owner does not match
@ -974,7 +1016,7 @@ describe("increase_liquidity", () => {
});
it("fails when tick arrays do not match the position", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: ZERO_BN }],
});
@ -984,17 +1026,18 @@ describe("increase_liquidity", () => {
const {
params: { tickArrayPda: tickArrayLowerPda },
} = await initTickArray(client, whirlpoolPda.publicKey, 11264);
} = await initTickArray(ctx, whirlpoolPda.publicKey, 11264);
const {
params: { tickArrayPda: tickArrayUpperPda },
} = await initTickArray(client, whirlpoolPda.publicKey, 22528);
} = await initTickArray(ctx, whirlpoolPda.publicKey, 22528);
const liquidityAmount = new u64(1_250_000);
await assert.rejects(
client
.increaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.increaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMaxA: new u64(0),
tokenMaxB: new u64(167_000),
@ -1009,13 +1052,13 @@ describe("increase_liquidity", () => {
tickArrayLower: tickArrayLowerPda.publicKey,
tickArrayUpper: tickArrayUpperPda.publicKey,
})
.buildAndExecute(),
).buildAndExecute(),
/0x1779/ // TicKNotFound
);
});
it("fails when the tick arrays are for a different whirlpool", async () => {
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
positions: [{ tickLowerIndex: -1280, tickUpperIndex: 1280, liquidityAmount: ZERO_BN }],
});
@ -1023,21 +1066,22 @@ describe("increase_liquidity", () => {
const { whirlpoolPda } = poolInitInfo;
const positionInitInfo = positions[0];
const { poolInitInfo: poolInitInfo2 } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo: poolInitInfo2 } = await initTestPool(ctx, TickSpacing.Standard);
const {
params: { tickArrayPda: tickArrayLowerPda },
} = await initTickArray(client, poolInitInfo2.whirlpoolPda.publicKey, -11264);
} = await initTickArray(ctx, poolInitInfo2.whirlpoolPda.publicKey, -11264);
const {
params: { tickArrayPda: tickArrayUpperPda },
} = await initTickArray(client, poolInitInfo2.whirlpoolPda.publicKey, 0);
} = await initTickArray(ctx, poolInitInfo2.whirlpoolPda.publicKey, 0);
const liquidityAmount = new u64(1_250_000);
await assert.rejects(
client
.increaseLiquidityTx({
toTx(
ctx,
WhirlpoolIx.increaseLiquidityIx(ctx.program, {
liquidityAmount,
tokenMaxA: new u64(0),
tokenMaxB: new u64(167_000),
@ -1052,7 +1096,7 @@ describe("increase_liquidity", () => {
tickArrayLower: tickArrayLowerPda.publicKey,
tickArrayUpper: tickArrayUpperPda.publicKey,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7d1/ // A has one constraint was violated
);
});

View File

@ -0,0 +1,70 @@
import * as assert from "assert";
import * as anchor from "@project-serum/anchor";
import {
WhirlpoolContext,
AccountFetcher,
WhirlpoolsConfigData,
WhirlpoolIx,
InitConfigParams,
} from "../../src";
import { systemTransferTx, ONE_SOL } from "../utils";
import { generateDefaultConfigParams } from "../utils/test-builders";
import { toTx } from "../../src/utils/instructions-util";
describe("initialize_config", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
let initializedConfigInfo: InitConfigParams;
it("successfully init a WhirlpoolsConfig account", async () => {
const { configInitInfo } = generateDefaultConfigParams(ctx);
await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configInitInfo)).buildAndExecute();
const configAccount = (await fetcher.getConfig(
configInitInfo.whirlpoolsConfigKeypair.publicKey
)) as WhirlpoolsConfigData;
assert.ok(
configAccount.collectProtocolFeesAuthority.equals(configInitInfo.collectProtocolFeesAuthority)
);
assert.ok(configAccount.feeAuthority.equals(configInitInfo.feeAuthority));
assert.ok(
configAccount.rewardEmissionsSuperAuthority.equals(
configInitInfo.rewardEmissionsSuperAuthority
)
);
assert.equal(configAccount.defaultProtocolFeeRate, configInitInfo.defaultProtocolFeeRate);
initializedConfigInfo = configInitInfo;
});
it("fail on passing in already initialized whirlpool account", async () => {
let infoWithDupeConfigKey = {
...generateDefaultConfigParams(ctx).configInitInfo,
whirlpoolsConfigKeypair: initializedConfigInfo.whirlpoolsConfigKeypair,
};
await assert.rejects(
toTx(
ctx,
WhirlpoolIx.initializeConfigIx(ctx.program, infoWithDupeConfigKey)
).buildAndExecute(),
/0x0/
);
});
it("succeeds when funder is different than account paying for transaction fee", async () => {
const funderKeypair = anchor.web3.Keypair.generate();
await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute();
const { configInitInfo } = generateDefaultConfigParams(ctx, funderKeypair.publicKey);
await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configInitInfo))
.addSigner(funderKeypair)
.buildAndExecute();
});
});

View File

@ -1,74 +1,73 @@
import * as assert from "assert";
import * as anchor from "@project-serum/anchor";
import { WhirlpoolContext } from "../src/context";
import { WhirlpoolClient } from "../src/client";
import { initFeeTier } from "./utils/init-utils";
import { WhirlpoolContext, AccountFetcher, FeeTierData, WhirlpoolIx, PDAUtil } from "../../src";
import { TickSpacing, systemTransferTx, ONE_SOL } from "../utils";
import { initFeeTier } from "../utils/init-utils";
import {
generateDefaultConfigParams,
generateDefaultInitFeeTierParams,
} from "./utils/test-builders";
import { getFeeTierPda } from "../src";
import { systemTransferTx, ONE_SOL, TickSpacing } from "./utils";
} from "../utils/test-builders";
import { toTx } from "../../src/utils/instructions-util";
describe("initialize_fee_tier", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
it("successfully init a FeeRate stable account", async () => {
const { configInitInfo, configKeypairs } = generateDefaultConfigParams(context);
await client.initConfigTx(configInitInfo).buildAndExecute();
const { configInitInfo, configKeypairs } = generateDefaultConfigParams(ctx);
await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configInitInfo)).buildAndExecute();
const testTickSpacing = TickSpacing.Stable;
const { params } = await initFeeTier(
client,
ctx,
configInitInfo,
configKeypairs.feeAuthorityKeypair,
testTickSpacing,
800
);
const generatedPda = getFeeTierPda(
client.context.program.programId,
configInitInfo.whirlpoolConfigKeypair.publicKey,
const generatedPda = PDAUtil.getFeeTier(
ctx.program.programId,
configInitInfo.whirlpoolsConfigKeypair.publicKey,
testTickSpacing
);
const feeTierAccount = await client.getFeeTier(generatedPda.publicKey);
const feeTierAccount = (await fetcher.getFeeTier(generatedPda.publicKey)) as FeeTierData;
assert.ok(feeTierAccount.tickSpacing == params.tickSpacing);
assert.ok(feeTierAccount.defaultFeeRate == params.defaultFeeRate);
});
it("successfully init a FeeRate standard account", async () => {
const { configInitInfo, configKeypairs } = generateDefaultConfigParams(context);
await client.initConfigTx(configInitInfo).buildAndExecute();
const { configInitInfo, configKeypairs } = generateDefaultConfigParams(ctx);
await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configInitInfo)).buildAndExecute();
const testTickSpacing = TickSpacing.Standard;
const { params } = await initFeeTier(
client,
ctx,
configInitInfo,
configKeypairs.feeAuthorityKeypair,
testTickSpacing,
3000
);
const feeTierAccount = await client.getFeeTier(params.feeTierPda.publicKey);
const feeTierAccount = (await fetcher.getFeeTier(params.feeTierPda.publicKey)) as FeeTierData;
assert.ok(feeTierAccount.tickSpacing == params.tickSpacing);
assert.ok(feeTierAccount.defaultFeeRate == params.defaultFeeRate);
});
it("successfully init a FeeRate with another funder wallet", async () => {
const { configInitInfo, configKeypairs } = generateDefaultConfigParams(context);
await client.initConfigTx(configInitInfo).buildAndExecute();
const { configInitInfo, configKeypairs } = generateDefaultConfigParams(ctx);
await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configInitInfo)).buildAndExecute();
const funderKeypair = anchor.web3.Keypair.generate();
await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute();
await initFeeTier(
client,
ctx,
configInitInfo,
configKeypairs.feeAuthorityKeypair,
TickSpacing.Stable,
@ -78,12 +77,12 @@ describe("initialize_fee_tier", () => {
});
it("fails when default fee rate exceeds max", async () => {
const { configInitInfo, configKeypairs } = generateDefaultConfigParams(context);
await client.initConfigTx(configInitInfo).buildAndExecute();
const { configInitInfo, configKeypairs } = generateDefaultConfigParams(ctx);
await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configInitInfo)).buildAndExecute();
await assert.rejects(
initFeeTier(
client,
ctx,
configInitInfo,
configKeypairs.feeAuthorityKeypair,
TickSpacing.Stable,
@ -94,41 +93,46 @@ describe("initialize_fee_tier", () => {
});
it("fails when fee authority is not a signer", async () => {
const { configInitInfo } = generateDefaultConfigParams(context);
await client.initConfigTx(configInitInfo).buildAndExecute();
const { configInitInfo } = generateDefaultConfigParams(ctx);
await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configInitInfo)).buildAndExecute();
await assert.rejects(
client
.initFeeTierTx(
toTx(
ctx,
WhirlpoolIx.initializeFeeTierIx(
ctx.program,
generateDefaultInitFeeTierParams(
client.context,
configInitInfo.whirlpoolConfigKeypair.publicKey,
ctx,
configInitInfo.whirlpoolsConfigKeypair.publicKey,
configInitInfo.feeAuthority,
TickSpacing.Stable,
3000
)
)
.buildAndExecute(),
).buildAndExecute(),
/Signature verification failed/
);
});
it("fails when invalid fee authority provided", async () => {
const { configInitInfo } = generateDefaultConfigParams(context);
await client.initConfigTx(configInitInfo).buildAndExecute();
const { configInitInfo } = generateDefaultConfigParams(ctx);
await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configInitInfo)).buildAndExecute();
const fakeFeeAuthorityKeypair = anchor.web3.Keypair.generate();
await assert.rejects(
client
.initFeeTierTx(
toTx(
ctx,
WhirlpoolIx.initializeFeeTierIx(
ctx.program,
generateDefaultInitFeeTierParams(
client.context,
configInitInfo.whirlpoolConfigKeypair.publicKey,
ctx,
configInitInfo.whirlpoolsConfigKeypair.publicKey,
fakeFeeAuthorityKeypair.publicKey,
TickSpacing.Stable,
3000
)
)
)
.addSigner(fakeFeeAuthorityKeypair)
.buildAndExecute(),
/0x7dc/ // ConstraintAddress

View File

@ -1,45 +1,48 @@
import * as assert from "assert";
import * as anchor from "@project-serum/anchor";
import Decimal from "decimal.js";
import {
createMint,
asyncAssertTokenVault,
ZERO_BN,
systemTransferTx,
ONE_SOL,
TickSpacing,
} from "./utils";
import { buildTestPoolParams, initTestPool } from "./utils/init-utils";
import { WhirlpoolClient } from "../src/client";
import { WhirlpoolContext } from "../src/context";
import {
getWhirlpoolPda,
WhirlpoolContext,
AccountFetcher,
WhirlpoolData,
InitPoolParams,
toX64,
sqrtPriceX64ToTickIndex,
MAX_SQRT_PRICE,
MIN_SQRT_PRICE,
} from "../src";
import Decimal from "decimal.js";
PriceMath,
WhirlpoolIx,
PDAUtil,
} from "../../src";
import {
TickSpacing,
ZERO_BN,
asyncAssertTokenVault,
systemTransferTx,
ONE_SOL,
createMint,
} from "../utils";
import { initTestPool, buildTestPoolParams } from "../utils/init-utils";
import { MathUtil } from "@orca-so/common-sdk";
import { toTx } from "../../src/utils/instructions-util";
describe("initialize_pool", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
it("successfully init a Standard account", async () => {
const price = toX64(new Decimal(5));
const price = MathUtil.toX64(new Decimal(5));
const { configInitInfo, poolInitInfo, feeTierParams } = await initTestPool(
client,
ctx,
TickSpacing.Standard,
price
);
const whirlpool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey);
const whirlpool = (await fetcher.getPool(poolInitInfo.whirlpoolPda.publicKey)) as WhirlpoolData;
const expectedWhirlpoolPda = getWhirlpoolPda(
const expectedWhirlpoolPda = PDAUtil.getWhirlpool(
program.programId,
configInitInfo.whirlpoolConfigKeypair.publicKey,
configInitInfo.whirlpoolsConfigKeypair.publicKey,
poolInitInfo.tokenMintA,
poolInitInfo.tokenMintB,
TickSpacing.Standard
@ -48,7 +51,7 @@ describe("initialize_pool", () => {
assert.ok(poolInitInfo.whirlpoolPda.publicKey.equals(expectedWhirlpoolPda.publicKey));
assert.equal(expectedWhirlpoolPda.bump, whirlpool.whirlpoolBump[0]);
assert.ok(whirlpool.whirlpoolsConfig.equals(poolInitInfo.whirlpoolConfigKey));
assert.ok(whirlpool.whirlpoolsConfig.equals(poolInitInfo.whirlpoolsConfig));
assert.ok(whirlpool.tokenMintA.equals(poolInitInfo.tokenMintA));
assert.ok(whirlpool.tokenVaultA.equals(poolInitInfo.tokenVaultAKeypair.publicKey));
@ -61,7 +64,10 @@ describe("initialize_pool", () => {
assert.ok(whirlpool.sqrtPrice.eq(new anchor.BN(poolInitInfo.initSqrtPrice.toString())));
assert.ok(whirlpool.liquidity.eq(ZERO_BN));
assert.equal(whirlpool.tickCurrentIndex, sqrtPriceX64ToTickIndex(poolInitInfo.initSqrtPrice));
assert.equal(
whirlpool.tickCurrentIndex,
PriceMath.sqrtPriceX64ToTickIndex(poolInitInfo.initSqrtPrice)
);
assert.ok(whirlpool.protocolFeeOwedA.eq(ZERO_BN));
assert.ok(whirlpool.protocolFeeOwedB.eq(ZERO_BN));
@ -89,15 +95,15 @@ describe("initialize_pool", () => {
});
it("successfully init a Stable account", async () => {
const price = toX64(new Decimal(5));
const price = MathUtil.toX64(new Decimal(5));
const { configInitInfo, poolInitInfo, feeTierParams } = await initTestPool(
client,
ctx,
TickSpacing.Stable,
price
);
const whirlpool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey);
const whirlpool = (await fetcher.getPool(poolInitInfo.whirlpoolPda.publicKey)) as WhirlpoolData;
assert.ok(whirlpool.whirlpoolsConfig.equals(poolInitInfo.whirlpoolConfigKey));
assert.ok(whirlpool.whirlpoolsConfig.equals(poolInitInfo.whirlpoolsConfig));
assert.ok(whirlpool.tokenMintA.equals(poolInitInfo.tokenMintA));
assert.ok(whirlpool.tokenVaultA.equals(poolInitInfo.tokenVaultAKeypair.publicKey));
@ -110,7 +116,10 @@ describe("initialize_pool", () => {
assert.ok(whirlpool.sqrtPrice.eq(new anchor.BN(poolInitInfo.initSqrtPrice.toString())));
assert.ok(whirlpool.liquidity.eq(ZERO_BN));
assert.equal(whirlpool.tickCurrentIndex, sqrtPriceX64ToTickIndex(poolInitInfo.initSqrtPrice));
assert.equal(
whirlpool.tickCurrentIndex,
PriceMath.sqrtPriceX64ToTickIndex(poolInitInfo.initSqrtPrice)
);
assert.ok(whirlpool.protocolFeeOwedA.eq(ZERO_BN));
assert.ok(whirlpool.protocolFeeOwedB.eq(ZERO_BN));
@ -140,11 +149,11 @@ describe("initialize_pool", () => {
it("succeeds when funder is different than account paying for transaction fee", async () => {
const funderKeypair = anchor.web3.Keypair.generate();
await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute();
await initTestPool(client, TickSpacing.Standard, toX64(new Decimal(5)), funderKeypair);
await initTestPool(ctx, TickSpacing.Standard, MathUtil.toX64(new Decimal(5)), funderKeypair);
});
it("fails when tokenVaultA mint does not match tokenA mint", async () => {
const { poolInitInfo } = await buildTestPoolParams(client, TickSpacing.Standard);
const { poolInitInfo } = await buildTestPoolParams(ctx, TickSpacing.Standard);
const otherTokenPublicKey = await createMint(provider);
const modifiedPoolInitInfo: InitPoolParams = {
@ -153,13 +162,13 @@ describe("initialize_pool", () => {
};
await assert.rejects(
client.initPoolTx(modifiedPoolInitInfo).buildAndExecute(),
toTx(ctx, WhirlpoolIx.initializePoolIx(ctx.program, modifiedPoolInitInfo)).buildAndExecute(),
/failed to complete|seeds|unauthorized/
);
});
it("fails when tokenVaultB mint does not match tokenB mint", async () => {
const { poolInitInfo } = await buildTestPoolParams(client, TickSpacing.Standard);
const { poolInitInfo } = await buildTestPoolParams(ctx, TickSpacing.Standard);
const otherTokenPublicKey = await createMint(provider);
const modifiedPoolInitInfo: InitPoolParams = {
@ -168,20 +177,17 @@ describe("initialize_pool", () => {
};
await assert.rejects(
client.initPoolTx(modifiedPoolInitInfo).buildAndExecute(),
toTx(ctx, WhirlpoolIx.initializePoolIx(ctx.program, modifiedPoolInitInfo)).buildAndExecute(),
/failed to complete|seeds|unauthorized/
);
});
it("fails when token mints are in the wrong order", async () => {
const { poolInitInfo, configInitInfo } = await buildTestPoolParams(
client,
TickSpacing.Standard
);
const { poolInitInfo, configInitInfo } = await buildTestPoolParams(ctx, TickSpacing.Standard);
const whirlpoolPda = getWhirlpoolPda(
context.program.programId,
configInitInfo.whirlpoolConfigKeypair.publicKey,
const whirlpoolPda = PDAUtil.getWhirlpool(
ctx.program.programId,
configInitInfo.whirlpoolsConfigKeypair.publicKey,
poolInitInfo.tokenMintB,
poolInitInfo.tokenMintA,
TickSpacing.Stable
@ -196,20 +202,17 @@ describe("initialize_pool", () => {
};
await assert.rejects(
client.initPoolTx(modifiedPoolInitInfo).buildAndExecute(),
toTx(ctx, WhirlpoolIx.initializePoolIx(ctx.program, modifiedPoolInitInfo)).buildAndExecute(),
/custom program error: 0x1788/ // InvalidTokenMintOrder
);
});
it("fails when the same token mint is passed in", async () => {
const { poolInitInfo, configInitInfo } = await buildTestPoolParams(
client,
TickSpacing.Standard
);
const { poolInitInfo, configInitInfo } = await buildTestPoolParams(ctx, TickSpacing.Standard);
const whirlpoolPda = getWhirlpoolPda(
context.program.programId,
configInitInfo.whirlpoolConfigKeypair.publicKey,
const whirlpoolPda = PDAUtil.getWhirlpool(
ctx.program.programId,
configInitInfo.whirlpoolsConfigKeypair.publicKey,
poolInitInfo.tokenMintA,
poolInitInfo.tokenMintA,
TickSpacing.Stable
@ -223,13 +226,13 @@ describe("initialize_pool", () => {
};
await assert.rejects(
client.initPoolTx(modifiedPoolInitInfo).buildAndExecute(),
toTx(ctx, WhirlpoolIx.initializePoolIx(ctx.program, modifiedPoolInitInfo)).buildAndExecute(),
/custom program error: 0x1788/ // InvalidTokenMintOrder
);
});
it("fails when sqrt-price exceeds max", async () => {
const { poolInitInfo } = await buildTestPoolParams(client, TickSpacing.Standard);
const { poolInitInfo } = await buildTestPoolParams(ctx, TickSpacing.Standard);
const otherTokenPublicKey = await createMint(provider);
const modifiedPoolInitInfo: InitPoolParams = {
@ -238,13 +241,13 @@ describe("initialize_pool", () => {
};
await assert.rejects(
client.initPoolTx(modifiedPoolInitInfo).buildAndExecute(),
toTx(ctx, WhirlpoolIx.initializePoolIx(ctx.program, modifiedPoolInitInfo)).buildAndExecute(),
/custom program error: 0x177b/ // SqrtPriceOutOfBounds
);
});
it("fails when sqrt-price subceeds min", async () => {
const { poolInitInfo } = await buildTestPoolParams(client, TickSpacing.Standard);
const { poolInitInfo } = await buildTestPoolParams(ctx, TickSpacing.Standard);
const modifiedPoolInitInfo: InitPoolParams = {
...poolInitInfo,
@ -252,7 +255,7 @@ describe("initialize_pool", () => {
};
await assert.rejects(
client.initPoolTx(modifiedPoolInitInfo).buildAndExecute(),
toTx(ctx, WhirlpoolIx.initializePoolIx(ctx.program, modifiedPoolInitInfo)).buildAndExecute(),
/custom program error: 0x177b/ // SqrtPriceOutOfBounds
);
});

View File

@ -1,36 +1,38 @@
import * as assert from "assert";
import * as anchor from "@project-serum/anchor";
import { WhirlpoolContext } from "../src/context";
import { WhirlpoolClient } from "../src/client";
import { initializeReward, initTestPool } from "./utils/init-utils";
import { createMint, ONE_SOL, systemTransferTx, TickSpacing } from "./utils";
import { WhirlpoolContext, AccountFetcher, WhirlpoolData, WhirlpoolIx } from "../../src";
import { TickSpacing, systemTransferTx, ONE_SOL, createMint } from "../utils";
import { initTestPool, initializeReward } from "../utils/init-utils";
import { toTx } from "../../src/utils/instructions-util";
describe("initialize_reward", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
it("successfully initializes reward at index 0", async () => {
const { poolInitInfo, configKeypairs } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo, configKeypairs } = await initTestPool(ctx, TickSpacing.Standard);
const { params } = await initializeReward(
client,
ctx,
configKeypairs.rewardEmissionsSuperAuthorityKeypair,
poolInitInfo.whirlpoolPda.publicKey,
0
);
const whirlpool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey);
const whirlpool = (await fetcher.getPool(
poolInitInfo.whirlpoolPda.publicKey,
true
)) as WhirlpoolData;
assert.ok(whirlpool.rewardInfos[0].mint.equals(params.rewardMint));
assert.ok(whirlpool.rewardInfos[0].vault.equals(params.rewardVaultKeypair.publicKey));
await assert.rejects(
initializeReward(
client,
ctx,
configKeypairs.rewardEmissionsSuperAuthorityKeypair,
poolInitInfo.whirlpoolPda.publicKey,
0
@ -39,13 +41,16 @@ describe("initialize_reward", () => {
);
const { params: params2 } = await initializeReward(
client,
ctx,
configKeypairs.rewardEmissionsSuperAuthorityKeypair,
poolInitInfo.whirlpoolPda.publicKey,
1
);
const whirlpool2 = await client.getPool(poolInitInfo.whirlpoolPda.publicKey);
const whirlpool2 = (await fetcher.getPool(
poolInitInfo.whirlpoolPda.publicKey,
true
)) as WhirlpoolData;
assert.ok(whirlpool2.rewardInfos[0].mint.equals(params.rewardMint));
assert.ok(whirlpool2.rewardInfos[0].vault.equals(params.rewardVaultKeypair.publicKey));
@ -56,11 +61,11 @@ describe("initialize_reward", () => {
});
it("succeeds when funder is different than account paying for transaction fee", async () => {
const { poolInitInfo, configKeypairs } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo, configKeypairs } = await initTestPool(ctx, TickSpacing.Standard);
const funderKeypair = anchor.web3.Keypair.generate();
await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute();
await initializeReward(
client,
ctx,
configKeypairs.rewardEmissionsSuperAuthorityKeypair,
poolInitInfo.whirlpoolPda.publicKey,
0,
@ -69,11 +74,11 @@ describe("initialize_reward", () => {
});
it("fails to initialize reward at index 1", async () => {
const { poolInitInfo, configKeypairs } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo, configKeypairs } = await initTestPool(ctx, TickSpacing.Standard);
await assert.rejects(
initializeReward(
client,
ctx,
configKeypairs.rewardEmissionsSuperAuthorityKeypair,
poolInitInfo.whirlpoolPda.publicKey,
1
@ -83,11 +88,11 @@ describe("initialize_reward", () => {
});
it("fails to initialize reward at out-of-bound index", async () => {
const { poolInitInfo, configKeypairs } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo, configKeypairs } = await initTestPool(ctx, TickSpacing.Standard);
await assert.rejects(
initializeReward(
client,
ctx,
configKeypairs.rewardEmissionsSuperAuthorityKeypair,
poolInitInfo.whirlpoolPda.publicKey,
3
@ -96,11 +101,12 @@ describe("initialize_reward", () => {
});
it("fails to initialize if authority signature is missing", async () => {
const { poolInitInfo, configKeypairs } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo, configKeypairs } = await initTestPool(ctx, TickSpacing.Standard);
await assert.rejects(
client
.initializeRewardTx({
toTx(
ctx,
WhirlpoolIx.initializeRewardIx(ctx.program, {
rewardAuthority: configKeypairs.rewardEmissionsSuperAuthorityKeypair.publicKey,
funder: provider.wallet.publicKey,
whirlpool: poolInitInfo.whirlpoolPda.publicKey,
@ -108,7 +114,7 @@ describe("initialize_reward", () => {
rewardVaultKeypair: anchor.web3.Keypair.generate(),
rewardIndex: 0,
})
.buildAndExecute()
).buildAndExecute()
);
});
});

View File

@ -1,75 +1,82 @@
import * as anchor from "@project-serum/anchor";
import * as assert from "assert";
import { WhirlpoolContext } from "../src/context";
import { WhirlpoolClient } from "../src/client";
import { initTestPool, initTickArray } from "./utils/init-utils";
import { generateDefaultInitTickArrayParams } from "./utils/test-builders";
import { InitPoolParams, InitTickArrayParams, TICK_ARRAY_SIZE } from "../src";
import { ONE_SOL, systemTransferTx, TickSpacing } from "./utils";
import {
WhirlpoolContext,
AccountFetcher,
TICK_ARRAY_SIZE,
InitTickArrayParams,
InitPoolParams,
TickArrayData,
WhirlpoolIx,
} from "../../src";
import { toTx } from "../../src/utils/instructions-util";
import { TickSpacing, systemTransferTx, ONE_SOL } from "../utils";
import { initTestPool, initTickArray } from "../utils/init-utils";
import { generateDefaultInitTickArrayParams } from "../utils/test-builders";
describe("initialize_tick_array", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
it("successfully init a TickArray account", async () => {
const tickSpacing = TickSpacing.Standard;
const { poolInitInfo } = await initTestPool(client, TickSpacing.Standard);
await client.getPool(poolInitInfo.whirlpoolPda.publicKey);
const { poolInitInfo } = await initTestPool(ctx, TickSpacing.Standard);
await fetcher.getPool(poolInitInfo.whirlpoolPda.publicKey);
const startTick = TICK_ARRAY_SIZE * tickSpacing * 2;
const tickArrayInitInfo = generateDefaultInitTickArrayParams(
context,
ctx,
poolInitInfo.whirlpoolPda.publicKey,
startTick
);
await client.initTickArrayTx(tickArrayInitInfo).buildAndExecute();
assertTickArrayInitialized(client, tickArrayInitInfo, poolInitInfo, startTick);
await toTx(ctx, WhirlpoolIx.initTickArrayIx(ctx.program, tickArrayInitInfo)).buildAndExecute();
assertTickArrayInitialized(ctx, tickArrayInitInfo, poolInitInfo, startTick);
});
it("successfully init a TickArray account with a negative index", async () => {
const tickSpacing = TickSpacing.Standard;
const { poolInitInfo } = await initTestPool(client, TickSpacing.Standard);
await client.getPool(poolInitInfo.whirlpoolPda.publicKey);
const { poolInitInfo } = await initTestPool(ctx, TickSpacing.Standard);
await fetcher.getPool(poolInitInfo.whirlpoolPda.publicKey);
const startTick = TICK_ARRAY_SIZE * tickSpacing * -2;
const tickArrayInitInfo = generateDefaultInitTickArrayParams(
context,
ctx,
poolInitInfo.whirlpoolPda.publicKey,
startTick
);
await client.initTickArrayTx(tickArrayInitInfo).buildAndExecute();
assertTickArrayInitialized(client, tickArrayInitInfo, poolInitInfo, startTick);
await toTx(ctx, WhirlpoolIx.initTickArrayIx(ctx.program, tickArrayInitInfo)).buildAndExecute();
assertTickArrayInitialized(ctx, tickArrayInitInfo, poolInitInfo, startTick);
});
it("succeeds when funder is different than account paying for transaction fee", async () => {
const tickSpacing = TickSpacing.Standard;
const { poolInitInfo } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo } = await initTestPool(ctx, TickSpacing.Standard);
const funderKeypair = anchor.web3.Keypair.generate();
await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute();
await client.getPool(poolInitInfo.whirlpoolPda.publicKey);
await fetcher.getPool(poolInitInfo.whirlpoolPda.publicKey);
const startTick = TICK_ARRAY_SIZE * tickSpacing * 3;
await initTickArray(client, poolInitInfo.whirlpoolPda.publicKey, startTick, funderKeypair);
await initTickArray(ctx, poolInitInfo.whirlpoolPda.publicKey, startTick, funderKeypair);
});
it("fails when start tick index is not a valid start index", async () => {
const tickSpacing = TickSpacing.Standard;
const { poolInitInfo } = await initTestPool(client, TickSpacing.Standard);
await client.getPool(poolInitInfo.whirlpoolPda.publicKey);
const { poolInitInfo } = await initTestPool(ctx, TickSpacing.Standard);
await fetcher.getPool(poolInitInfo.whirlpoolPda.publicKey);
const startTick = TICK_ARRAY_SIZE * tickSpacing * 2 + 1;
const params = generateDefaultInitTickArrayParams(
context,
ctx,
poolInitInfo.whirlpoolPda.publicKey,
startTick
);
try {
await client.initTickArrayTx(params).buildAndExecute();
await toTx(ctx, WhirlpoolIx.initTickArrayIx(ctx.program, params)).buildAndExecute();
assert.fail(
"should fail if start-tick is not a multiple of tick spacing and num ticks in array"
);
@ -80,12 +87,14 @@ describe("initialize_tick_array", () => {
});
async function assertTickArrayInitialized(
client: WhirlpoolClient,
ctx: WhirlpoolContext,
tickArrayInitInfo: InitTickArrayParams,
poolInitInfo: InitPoolParams,
startTick: number
) {
let tickArrayData = await client.getTickArray(tickArrayInitInfo.tickArrayPda.publicKey);
let tickArrayData = (await fetcher.getTickArray(
tickArrayInitInfo.tickArrayPda.publicKey
)) as TickArrayData;
assert.ok(tickArrayData.whirlpool.equals(poolInitInfo.whirlpoolPda.publicKey));
assert.ok(tickArrayData.startTickIndex == startTick);
}

View File

@ -1,43 +1,45 @@
import * as assert from "assert";
import * as anchor from "@project-serum/anchor";
import { WhirlpoolContext } from "../../src/context";
import { WhirlpoolClient } from "../../src/client";
import { initTestPool, openPosition } from "../utils/init-utils";
import { buildOpenPositionIx } from "../../src/instructions/open-position-ix";
import { generateDefaultOpenPositionParams } from "../utils/test-builders";
import { TickSpacing } from "../utils";
import { WhirlpoolContext } from "../../../src/context";
import { initTestPool, openPosition } from "../../utils/init-utils";
import { generateDefaultOpenPositionParams } from "../../utils/test-builders";
import { TickSpacing } from "../../utils";
import { AccountFetcher, WhirlpoolIx } from "../../../src";
import { toTx } from "../../../src/utils/instructions-util";
describe("position management tests", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
it("successfully closes and opens a position in one transaction", async () => {
const { poolInitInfo } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo } = await initTestPool(ctx, TickSpacing.Standard);
const { params } = await openPosition(client, poolInitInfo.whirlpoolPda.publicKey, 0, 128);
const { params } = await openPosition(ctx, poolInitInfo.whirlpoolPda.publicKey, 0, 128);
const receiverKeypair = anchor.web3.Keypair.generate();
const { params: newParams, mint } = await generateDefaultOpenPositionParams(
context,
ctx,
poolInitInfo.whirlpoolPda.publicKey,
0,
128,
context.wallet.publicKey,
context.wallet.publicKey
ctx.wallet.publicKey,
ctx.wallet.publicKey
);
await client
.closePositionTx({
await toTx(
ctx,
WhirlpoolIx.closePositionIx(ctx.program, {
positionAuthority: provider.wallet.publicKey,
receiver: receiverKeypair.publicKey,
position: params.positionPda.publicKey,
positionMint: params.positionMintAddress,
positionTokenAccount: params.positionTokenAccountAddress,
positionTokenAccount: params.positionTokenAccount,
})
.addInstruction(buildOpenPositionIx(context, newParams))
)
.addInstruction(WhirlpoolIx.openPositionIx(ctx.program, newParams))
.addSigner(mint)
.buildAndExecute();

View File

@ -2,36 +2,39 @@ import * as assert from "assert";
import * as anchor from "@project-serum/anchor";
import { web3 } from "@project-serum/anchor";
import { Keypair } from "@solana/web3.js";
import { WhirlpoolClient } from "../src/client";
import { WhirlpoolContext } from "../src/context";
import { initTestPool, openPosition } from "./utils/init-utils";
import {
getPositionPda,
InitPoolParams,
MAX_TICK_INDEX,
MIN_TICK_INDEX,
OpenPositionParams,
PDA,
} from "../src";
import {
createMint,
createMintInstructions,
mintToByAuthority,
ONE_SOL,
systemTransferTx,
TickSpacing,
ZERO_BN,
} from "./utils";
import { ASSOCIATED_TOKEN_PROGRAM_ID, Token } from "@solana/spl-token";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { generateDefaultOpenPositionParams } from "./utils/test-builders";
import {
WhirlpoolContext,
AccountFetcher,
OpenPositionParams,
InitPoolParams,
PositionData,
MAX_TICK_INDEX,
MIN_TICK_INDEX,
WhirlpoolIx,
PDAUtil,
} from "../../src";
import {
TickSpacing,
systemTransferTx,
ONE_SOL,
ZERO_BN,
mintToByAuthority,
createMint,
createMintInstructions,
} from "../utils";
import { initTestPool, openPosition } from "../utils/init-utils";
import { generateDefaultOpenPositionParams } from "../utils/test-builders";
import { PDA } from "@orca-so/common-sdk";
import { toTx } from "../../src/utils/instructions-util";
describe("open_position", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
let defaultParams: OpenPositionParams;
let defaultMint: Keypair;
@ -42,11 +45,11 @@ describe("open_position", () => {
const funderKeypair = anchor.web3.Keypair.generate();
before(async () => {
poolInitInfo = (await initTestPool(client, TickSpacing.Standard)).poolInitInfo;
poolInitInfo = (await initTestPool(ctx, TickSpacing.Standard)).poolInitInfo;
whirlpoolPda = poolInitInfo.whirlpoolPda;
const { params, mint } = await generateDefaultOpenPositionParams(
client.context,
ctx,
whirlpoolPda.publicKey,
0,
128,
@ -59,14 +62,14 @@ describe("open_position", () => {
it("successfully opens position and verify position address contents", async () => {
const positionInitInfo = await openPosition(
client,
ctx,
whirlpoolPda.publicKey,
tickLowerIndex,
tickUpperIndex
);
const { positionPda, positionMintAddress } = positionInitInfo.params;
const position = await client.getPosition(positionPda.publicKey);
const position = (await fetcher.getPosition(positionPda.publicKey)) as PositionData;
assert.strictEqual(position.tickLowerIndex, tickLowerIndex);
assert.strictEqual(position.tickUpperIndex, tickUpperIndex);
@ -83,7 +86,7 @@ describe("open_position", () => {
it("succeeds when funder is different than account paying for transaction fee", async () => {
await openPosition(
client,
ctx,
whirlpoolPda.publicKey,
tickLowerIndex,
tickUpperIndex,
@ -96,16 +99,17 @@ describe("open_position", () => {
const newOwner = web3.Keypair.generate();
const positionInitInfo = await openPosition(
client,
ctx,
whirlpoolPda.publicKey,
tickLowerIndex,
tickUpperIndex,
newOwner.publicKey
);
const { positionMintAddress, positionTokenAccountAddress } = positionInitInfo.params;
const { positionMintAddress, positionTokenAccount: positionTokenAccountAddress } =
positionInitInfo.params;
const token = new Token(
context.connection,
ctx.connection,
positionMintAddress,
TOKEN_PROGRAM_ID,
web3.Keypair.generate()
@ -127,12 +131,17 @@ describe("open_position", () => {
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
anotherMintKey,
context.provider.wallet.publicKey
ctx.provider.wallet.publicKey
);
await assert.rejects(
client
.openPositionTx({ ...defaultParams, positionTokenAccountAddress })
toTx(
ctx,
WhirlpoolIx.openPositionIx(ctx.program, {
...defaultParams,
positionTokenAccount: positionTokenAccountAddress,
})
)
.addSigner(defaultMint)
.buildAndExecute(),
/An account required by the instruction is missing/
@ -143,7 +152,7 @@ describe("open_position", () => {
async function assertTicksFail(lowerTick: number, upperTick: number) {
await assert.rejects(
openPosition(
client,
ctx,
whirlpoolPda.publicKey,
lowerTick,
upperTick,
@ -181,7 +190,7 @@ describe("open_position", () => {
it("fail when position mint already exists", async () => {
const positionMintKeypair = anchor.web3.Keypair.generate();
const positionPda = getPositionPda(context.program.programId, positionMintKeypair.publicKey);
const positionPda = PDAUtil.getPosition(ctx.program.programId, positionMintKeypair.publicKey);
const positionTokenAccountAddress = await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
@ -202,17 +211,19 @@ describe("open_position", () => {
await provider.send(tx, [positionMintKeypair], { commitment: "confirmed" });
await assert.rejects(
client
.openPositionTx({
toTx(
ctx,
WhirlpoolIx.openPositionIx(ctx.program, {
funder: provider.wallet.publicKey,
ownerKey: provider.wallet.publicKey,
owner: provider.wallet.publicKey,
positionPda,
positionMintAddress: positionMintKeypair.publicKey,
positionTokenAccountAddress: positionTokenAccountAddress,
whirlpoolKey: whirlpoolPda.publicKey,
positionTokenAccount: positionTokenAccountAddress,
whirlpool: whirlpoolPda.publicKey,
tickLowerIndex: 0,
tickUpperIndex: 128,
})
)
.addSigner(positionMintKeypair)
.buildAndExecute(),
/0x0/

View File

@ -3,43 +3,43 @@ import * as anchor from "@project-serum/anchor";
import { Metadata } from "@metaplex-foundation/mpl-token-metadata";
import { web3 } from "@project-serum/anchor";
import { Keypair, PublicKey } from "@solana/web3.js";
import { WhirlpoolClient } from "../src/client";
import { WhirlpoolContext } from "../src/context";
import { initTestPool, openPositionWithMetadata } from "./utils/init-utils";
import {
getPositionMetadataPda,
getPositionPda,
InitPoolParams,
MAX_TICK_INDEX,
METADATA_PROGRAM_ADDRESS,
MIN_TICK_INDEX,
OpenPositionParams,
OpenPositionWithMetadataBumpsData,
PDA,
TransactionBuilder,
} from "../src";
import { openPositionAccounts } from "../src/instructions/open-position-ix";
import {
createMint,
createMintInstructions,
mintToByAuthority,
ONE_SOL,
systemTransferTx,
TickSpacing,
ZERO_BN,
} from "./utils";
import { ASSOCIATED_TOKEN_PROGRAM_ID, Token } from "@solana/spl-token";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { generateDefaultOpenPositionParams } from "./utils/test-builders";
import {
WhirlpoolContext,
AccountFetcher,
OpenPositionParams,
InitPoolParams,
PositionData,
MAX_TICK_INDEX,
MIN_TICK_INDEX,
OpenPositionWithMetadataBumpsData,
METADATA_PROGRAM_ADDRESS,
WhirlpoolIx,
PDAUtil,
} from "../../src";
import {
TickSpacing,
systemTransferTx,
ONE_SOL,
ZERO_BN,
mintToByAuthority,
createMint,
createMintInstructions,
} from "../utils";
import { initTestPool, openPositionWithMetadata } from "../utils/init-utils";
import { generateDefaultOpenPositionParams } from "../utils/test-builders";
import { openPositionAccounts, toTx } from "../../src/utils/instructions-util";
import { PDA, TransactionBuilder } from "@orca-so/common-sdk";
describe("open_position_with_metadata", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
let defaultParams: Required<OpenPositionParams>;
let defaultParams: Required<OpenPositionParams & { metadataPda: PDA }>;
let defaultMint: Keypair;
const tickLowerIndex = 0;
const tickUpperIndex = 128;
@ -48,11 +48,11 @@ describe("open_position_with_metadata", () => {
const funderKeypair = anchor.web3.Keypair.generate();
before(async () => {
poolInitInfo = (await initTestPool(client, TickSpacing.Standard)).poolInitInfo;
poolInitInfo = (await initTestPool(ctx, TickSpacing.Standard)).poolInitInfo;
whirlpoolPda = poolInitInfo.whirlpoolPda;
const { params, mint } = await generateDefaultOpenPositionParams(
client.context,
ctx,
whirlpoolPda.publicKey,
0,
128,
@ -75,13 +75,13 @@ describe("open_position_with_metadata", () => {
it("successfully opens position and verify position address contents", async () => {
const positionInitInfo = await openPositionWithMetadata(
client,
ctx,
whirlpoolPda.publicKey,
tickLowerIndex,
tickUpperIndex
);
const { positionPda, metadataPda, positionMintAddress } = positionInitInfo.params;
const position = await client.getPosition(positionPda.publicKey);
const position = (await fetcher.getPosition(positionPda.publicKey)) as PositionData;
assert.strictEqual(position.tickLowerIndex, tickLowerIndex);
assert.strictEqual(position.tickUpperIndex, tickUpperIndex);
@ -99,7 +99,7 @@ describe("open_position_with_metadata", () => {
it("succeeds when funder is different than account paying for transaction fee", async () => {
const { params } = await openPositionWithMetadata(
client,
ctx,
whirlpoolPda.publicKey,
tickLowerIndex,
tickUpperIndex,
@ -114,17 +114,20 @@ describe("open_position_with_metadata", () => {
const newOwner = web3.Keypair.generate();
const positionInitInfo = await openPositionWithMetadata(
client,
ctx,
whirlpoolPda.publicKey,
tickLowerIndex,
tickUpperIndex,
newOwner.publicKey
);
const { metadataPda, positionMintAddress, positionTokenAccountAddress } =
positionInitInfo.params;
const {
metadataPda,
positionMintAddress,
positionTokenAccount: positionTokenAccountAddress,
} = positionInitInfo.params;
const token = new Token(
context.connection,
ctx.connection,
positionMintAddress,
TOKEN_PROGRAM_ID,
web3.Keypair.generate()
@ -148,12 +151,17 @@ describe("open_position_with_metadata", () => {
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
anotherMintKey,
context.provider.wallet.publicKey
ctx.provider.wallet.publicKey
);
await assert.rejects(
client
.openPositionWithMetadataTx({ ...defaultParams, positionTokenAccountAddress })
toTx(
ctx,
WhirlpoolIx.openPositionWithMetadataIx(ctx.program, {
...defaultParams,
positionTokenAccount: positionTokenAccountAddress,
})
)
.addSigner(defaultMint)
.buildAndExecute(),
/An account required by the instruction is missing/
@ -164,7 +172,7 @@ describe("open_position_with_metadata", () => {
async function assertTicksFail(lowerTick: number, upperTick: number) {
await assert.rejects(
openPositionWithMetadata(
client,
ctx,
whirlpoolPda.publicKey,
lowerTick,
upperTick,
@ -202,8 +210,8 @@ describe("open_position_with_metadata", () => {
it("fail when position mint already exists", async () => {
const positionMintKeypair = anchor.web3.Keypair.generate();
const positionPda = getPositionPda(context.program.programId, positionMintKeypair.publicKey);
const metadataPda = getPositionMetadataPda(positionMintKeypair.publicKey);
const positionPda = PDAUtil.getPosition(ctx.program.programId, positionMintKeypair.publicKey);
const metadataPda = PDAUtil.getPositionMetadata(positionMintKeypair.publicKey);
const positionTokenAccountAddress = await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
@ -224,17 +232,19 @@ describe("open_position_with_metadata", () => {
await provider.send(tx, [positionMintKeypair], { commitment: "confirmed" });
await assert.rejects(
client
.openPositionWithMetadataTx({
toTx(
ctx,
WhirlpoolIx.openPositionWithMetadataIx(ctx.program, {
...defaultParams,
positionPda,
metadataPda,
positionMintAddress: positionMintKeypair.publicKey,
positionTokenAccountAddress: positionTokenAccountAddress,
whirlpoolKey: whirlpoolPda.publicKey,
positionTokenAccount: positionTokenAccountAddress,
whirlpool: whirlpoolPda.publicKey,
tickLowerIndex,
tickUpperIndex,
})
)
.addSigner(positionMintKeypair)
.buildAndExecute(),
/0x0/
@ -258,7 +268,7 @@ describe("open_position_with_metadata", () => {
metadataBump: metadataPda.bump,
};
const ix = context.program.instruction.openPositionWithMetadata(
const ix = ctx.program.instruction.openPositionWithMetadata(
bumps,
tickLowerIndex,
tickUpperIndex,
@ -284,11 +294,13 @@ describe("open_position_with_metadata", () => {
const notMintKeypair = Keypair.generate();
const invalidParams = {
...defaultParams,
metadataPda: getPositionMetadataPda(notMintKeypair.publicKey),
metadataPda: PDAUtil.getPositionMetadata(notMintKeypair.publicKey),
};
await assert.rejects(
client.openPositionWithMetadataTx(invalidParams).addSigner(defaultMint).buildAndExecute(),
toTx(ctx, WhirlpoolIx.openPositionWithMetadataIx(ctx.program, invalidParams))
.addSigner(defaultMint)
.buildAndExecute(),
// Invalid Metadata Key
// https://github.com/metaplex-foundation/metaplex-program-library/blob/master/token-metadata/program/src/error.rs#L36
/0x5/
@ -297,7 +309,7 @@ describe("open_position_with_metadata", () => {
it("fails with non-program metadata program", async () => {
const notMetadataProgram = Keypair.generate();
const tx = new TransactionBuilder(context.provider).addInstruction(
const tx = new TransactionBuilder(ctx.provider).addInstruction(
buildOpenWithAccountOverrides({
metadataProgram: notMetadataProgram.publicKey,
})
@ -312,7 +324,7 @@ describe("open_position_with_metadata", () => {
});
it("fails with non-metadata program ", async () => {
const tx = new TransactionBuilder(context.provider).addInstruction(
const tx = new TransactionBuilder(ctx.provider).addInstruction(
buildOpenWithAccountOverrides({
metadataProgram: TOKEN_PROGRAM_ID,
})
@ -328,7 +340,7 @@ describe("open_position_with_metadata", () => {
it("fails with non-valid update_authority program", async () => {
const notUpdateAuth = Keypair.generate();
const tx = new TransactionBuilder(context.provider).addInstruction(
const tx = new TransactionBuilder(ctx.provider).addInstruction(
buildOpenWithAccountOverrides({
metadataUpdateAuth: notUpdateAuth.publicKey,
})

View File

@ -1,32 +1,36 @@
import * as assert from "assert";
import * as anchor from "@project-serum/anchor";
import { WhirlpoolContext } from "../src/context";
import { WhirlpoolClient } from "../src/client";
import { generateDefaultConfigParams } from "./utils/test-builders";
import { WhirlpoolContext, AccountFetcher, WhirlpoolsConfigData, WhirlpoolIx } from "../../src";
import { generateDefaultConfigParams } from "../utils/test-builders";
import { toTx } from "../../src/utils/instructions-util";
describe("set_collect_protocol_fee_authority", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
it("successfully set_collect_protocol_fee_authority", async () => {
const {
configInitInfo,
configKeypairs: { collectProtocolFeesAuthorityKeypair },
} = generateDefaultConfigParams(context);
await client.initConfigTx(configInitInfo).buildAndExecute();
} = generateDefaultConfigParams(ctx);
await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configInitInfo)).buildAndExecute();
const newAuthorityKeypair = anchor.web3.Keypair.generate();
await client
.setCollectProtocolFeesAuthorityTx({
whirlpoolsConfig: configInitInfo.whirlpoolConfigKeypair.publicKey,
await toTx(
ctx,
WhirlpoolIx.setCollectProtocolFeesAuthorityIx(ctx.program, {
whirlpoolsConfig: configInitInfo.whirlpoolsConfigKeypair.publicKey,
collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey,
newCollectProtocolFeesAuthority: newAuthorityKeypair.publicKey,
})
)
.addSigner(collectProtocolFeesAuthorityKeypair)
.buildAndExecute();
const config = await client.getConfig(configInitInfo.whirlpoolConfigKeypair.publicKey);
const config = (await fetcher.getConfig(
configInitInfo.whirlpoolsConfigKeypair.publicKey
)) as WhirlpoolsConfigData;
assert.ok(config.collectProtocolFeesAuthority.equals(newAuthorityKeypair.publicKey));
});
@ -34,33 +38,35 @@ describe("set_collect_protocol_fee_authority", () => {
const {
configInitInfo,
configKeypairs: { collectProtocolFeesAuthorityKeypair },
} = generateDefaultConfigParams(context);
await client.initConfigTx(configInitInfo).buildAndExecute();
} = generateDefaultConfigParams(ctx);
await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configInitInfo)).buildAndExecute();
await assert.rejects(
client
.setCollectProtocolFeesAuthorityTx({
whirlpoolsConfig: configInitInfo.whirlpoolConfigKeypair.publicKey,
toTx(
ctx,
WhirlpoolIx.setCollectProtocolFeesAuthorityIx(ctx.program, {
whirlpoolsConfig: configInitInfo.whirlpoolsConfigKeypair.publicKey,
collectProtocolFeesAuthority: collectProtocolFeesAuthorityKeypair.publicKey,
newCollectProtocolFeesAuthority: provider.wallet.publicKey,
})
.buildAndExecute(),
).buildAndExecute(),
/Signature verification failed/
);
});
it("fails if invalid collect_protocol_fee_authority provided", async () => {
const { configInitInfo } = generateDefaultConfigParams(context);
await client.initConfigTx(configInitInfo).buildAndExecute();
const { configInitInfo } = generateDefaultConfigParams(ctx);
await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configInitInfo)).buildAndExecute();
await assert.rejects(
client
.setCollectProtocolFeesAuthorityTx({
whirlpoolsConfig: configInitInfo.whirlpoolConfigKeypair.publicKey,
toTx(
ctx,
WhirlpoolIx.setCollectProtocolFeesAuthorityIx(ctx.program, {
whirlpoolsConfig: configInitInfo.whirlpoolsConfigKeypair.publicKey,
collectProtocolFeesAuthority: provider.wallet.publicKey,
newCollectProtocolFeesAuthority: provider.wallet.publicKey,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7dc/ // An address constraint was violated
);
});

View File

@ -1,52 +1,60 @@
import * as assert from "assert";
import * as anchor from "@project-serum/anchor";
import { getFeeTierPda, getWhirlpoolPda, InitPoolParams } from "../src";
import { initTestPool } from "./utils/init-utils";
import { WhirlpoolContext } from "../src/context";
import { WhirlpoolClient } from "../src/client";
import { createInOrderMints, generateDefaultConfigParams } from "./utils/test-builders";
import { TickSpacing } from "./utils";
import {
WhirlpoolContext,
AccountFetcher,
WhirlpoolData,
InitPoolParams,
WhirlpoolIx,
PDAUtil,
} from "../../src";
import { TickSpacing } from "../utils";
import { initTestPool } from "../utils/init-utils";
import { createInOrderMints, generateDefaultConfigParams } from "../utils/test-builders";
import { toTx } from "../../src/utils/instructions-util";
describe("set_default_fee_rate", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
it("successfully set_default_fee_rate", async () => {
const { poolInitInfo, configInitInfo, configKeypairs, feeTierParams } = await initTestPool(
client,
ctx,
TickSpacing.Standard
);
const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey;
const whirlpoolsConfigKey = configInitInfo.whirlpoolConfigKeypair.publicKey;
const whirlpoolsConfigKey = configInitInfo.whirlpoolsConfigKeypair.publicKey;
const feeAuthorityKeypair = configKeypairs.feeAuthorityKeypair;
const newDefaultFeeRate = 45;
// Fetch initial whirlpool and check it is default
let whirlpool_0 = await client.getPool(whirlpoolKey);
let whirlpool_0 = (await fetcher.getPool(whirlpoolKey)) as WhirlpoolData;
assert.equal(whirlpool_0.feeRate, feeTierParams.defaultFeeRate);
await client
.setDefaultFeeRateIx({
await toTx(
ctx,
WhirlpoolIx.setDefaultFeeRateIx(ctx.program, {
whirlpoolsConfig: whirlpoolsConfigKey,
feeAuthority: feeAuthorityKeypair.publicKey,
tickSpacing: TickSpacing.Standard,
defaultFeeRate: newDefaultFeeRate,
})
)
.addSigner(feeAuthorityKeypair)
.buildAndExecute();
// Setting the default rate did not change existing whirlpool fee rate
whirlpool_0 = await client.getPool(poolInitInfo.whirlpoolPda.publicKey);
whirlpool_0 = (await fetcher.getPool(poolInitInfo.whirlpoolPda.publicKey)) as WhirlpoolData;
assert.equal(whirlpool_0.feeRate, feeTierParams.defaultFeeRate);
// Newly initialized whirlpools have new default fee rate
const [tokenMintA, tokenMintB] = await createInOrderMints(context);
const whirlpoolPda = getWhirlpoolPda(
context.program.programId,
const [tokenMintA, tokenMintB] = await createInOrderMints(ctx);
const whirlpoolPda = PDAUtil.getWhirlpool(
ctx.program.programId,
whirlpoolsConfigKey,
tokenMintA,
tokenMintB,
@ -64,26 +72,28 @@ describe("set_default_fee_rate", () => {
tokenVaultBKeypair,
tickSpacing: TickSpacing.Stable,
};
await client.initPoolTx(newPoolInitInfo).buildAndExecute();
await toTx(ctx, WhirlpoolIx.initializePoolIx(ctx.program, newPoolInitInfo)).buildAndExecute();
const whirlpool_1 = await client.getPool(whirlpoolPda.publicKey);
const whirlpool_1 = (await fetcher.getPool(whirlpoolPda.publicKey)) as WhirlpoolData;
assert.equal(whirlpool_1.feeRate, newDefaultFeeRate);
});
it("fails when default fee rate exceeds max", async () => {
const { configInitInfo, configKeypairs } = await initTestPool(client, TickSpacing.Standard);
const whirlpoolsConfigKey = configInitInfo.whirlpoolConfigKeypair.publicKey;
const { configInitInfo, configKeypairs } = await initTestPool(ctx, TickSpacing.Standard);
const whirlpoolsConfigKey = configInitInfo.whirlpoolsConfigKeypair.publicKey;
const feeAuthorityKeypair = configKeypairs.feeAuthorityKeypair;
const newDefaultFeeRate = 20_000;
await assert.rejects(
client
.setDefaultFeeRateIx({
toTx(
ctx,
WhirlpoolIx.setDefaultFeeRateIx(ctx.program, {
whirlpoolsConfig: whirlpoolsConfigKey,
feeAuthority: feeAuthorityKeypair.publicKey,
tickSpacing: TickSpacing.Standard,
defaultFeeRate: newDefaultFeeRate,
})
)
.addSigner(feeAuthorityKeypair)
.buildAndExecute(),
/0x178c/ // FeeRateMaxExceeded
@ -91,18 +101,20 @@ describe("set_default_fee_rate", () => {
});
it("fails when fee tier account has not been initialized", async () => {
const { configInitInfo, configKeypairs } = generateDefaultConfigParams(context);
await client.initConfigTx(configInitInfo).buildAndExecute();
const { configInitInfo, configKeypairs } = generateDefaultConfigParams(ctx);
await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configInitInfo)).buildAndExecute();
const feeAuthorityKeypair = configKeypairs.feeAuthorityKeypair;
await assert.rejects(
client
.setDefaultFeeRateIx({
whirlpoolsConfig: configInitInfo.whirlpoolConfigKeypair.publicKey,
toTx(
ctx,
WhirlpoolIx.setDefaultFeeRateIx(ctx.program, {
whirlpoolsConfig: configInitInfo.whirlpoolsConfigKeypair.publicKey,
feeAuthority: feeAuthorityKeypair.publicKey,
tickSpacing: TickSpacing.Standard,
defaultFeeRate: 500,
})
)
.addSigner(feeAuthorityKeypair)
.buildAndExecute(),
/0xbc4/ // AccountNotInitialized
@ -110,12 +122,12 @@ describe("set_default_fee_rate", () => {
});
it("fails when fee authority is not a signer", async () => {
const { configInitInfo, configKeypairs } = await initTestPool(client, TickSpacing.Standard);
const whirlpoolsConfigKey = configInitInfo.whirlpoolConfigKeypair.publicKey;
const { configInitInfo, configKeypairs } = await initTestPool(ctx, TickSpacing.Standard);
const whirlpoolsConfigKey = configInitInfo.whirlpoolsConfigKeypair.publicKey;
const feeAuthorityKeypair = configKeypairs.feeAuthorityKeypair;
const feeTierPda = getFeeTierPda(
context.program.programId,
configInitInfo.whirlpoolConfigKeypair.publicKey,
const feeTierPda = PDAUtil.getFeeTier(
ctx.program.programId,
configInitInfo.whirlpoolsConfigKeypair.publicKey,
TickSpacing.Standard
);
@ -133,12 +145,12 @@ describe("set_default_fee_rate", () => {
});
it("fails when invalid fee authority provided", async () => {
const { configInitInfo } = await initTestPool(client, TickSpacing.Standard);
const whirlpoolsConfigKey = configInitInfo.whirlpoolConfigKeypair.publicKey;
const { configInitInfo } = await initTestPool(ctx, TickSpacing.Standard);
const whirlpoolsConfigKey = configInitInfo.whirlpoolsConfigKeypair.publicKey;
const fakeFeeAuthorityKeypair = anchor.web3.Keypair.generate();
const feeTierPda = getFeeTierPda(
context.program.programId,
configInitInfo.whirlpoolConfigKeypair.publicKey,
const feeTierPda = PDAUtil.getFeeTier(
ctx.program.programId,
configInitInfo.whirlpoolsConfigKeypair.publicKey,
TickSpacing.Standard
);

View File

@ -1,50 +1,58 @@
import * as assert from "assert";
import * as anchor from "@project-serum/anchor";
import { getWhirlpoolPda, InitPoolParams } from "../src";
import { WhirlpoolContext } from "../src/context";
import { WhirlpoolClient } from "../src/client";
import { initTestPool } from "./utils/init-utils";
import { createInOrderMints } from "./utils/test-builders";
import { TickSpacing } from "./utils";
import {
WhirlpoolContext,
AccountFetcher,
WhirlpoolData,
InitPoolParams,
WhirlpoolIx,
PDAUtil,
} from "../../src";
import { TickSpacing } from "../utils";
import { initTestPool } from "../utils/init-utils";
import { createInOrderMints } from "../utils/test-builders";
import { toTx } from "../../src/utils/instructions-util";
describe("set_default_protocol_fee_rate", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
it("successfully set_default_protocol_fee_rate", async () => {
const { poolInitInfo, configInitInfo, configKeypairs } = await initTestPool(
client,
ctx,
TickSpacing.Standard
);
const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey;
const whirlpoolsConfigKey = configInitInfo.whirlpoolConfigKeypair.publicKey;
const whirlpoolsConfigKey = configInitInfo.whirlpoolsConfigKeypair.publicKey;
const feeAuthorityKeypair = configKeypairs.feeAuthorityKeypair;
const newDefaultProtocolFeeRate = 45;
// Fetch initial whirlpool and check it is default
let whirlpool_0 = await client.getPool(whirlpoolKey);
let whirlpool_0 = (await fetcher.getPool(whirlpoolKey)) as WhirlpoolData;
assert.equal(whirlpool_0.protocolFeeRate, configInitInfo.defaultProtocolFeeRate);
await client
.setDefaultProtocolFeeRateIx({
await toTx(
ctx,
WhirlpoolIx.setDefaultProtocolFeeRateIx(ctx.program, {
whirlpoolsConfig: whirlpoolsConfigKey,
feeAuthority: feeAuthorityKeypair.publicKey,
defaultProtocolFeeRate: newDefaultProtocolFeeRate,
})
)
.addSigner(feeAuthorityKeypair)
.buildAndExecute();
// Setting the default rate did not change existing whirlpool fee rate
whirlpool_0 = await client.getPool(whirlpoolKey);
whirlpool_0 = (await fetcher.getPool(whirlpoolKey)) as WhirlpoolData;
assert.equal(whirlpool_0.protocolFeeRate, configInitInfo.defaultProtocolFeeRate);
const [tokenMintA, tokenMintB] = await createInOrderMints(context);
const whirlpoolPda = getWhirlpoolPda(
context.program.programId,
const [tokenMintA, tokenMintB] = await createInOrderMints(ctx);
const whirlpoolPda = PDAUtil.getWhirlpool(
ctx.program.programId,
whirlpoolsConfigKey,
tokenMintA,
tokenMintB,
@ -62,25 +70,27 @@ describe("set_default_protocol_fee_rate", () => {
tokenVaultBKeypair,
tickSpacing: TickSpacing.Stable,
};
await client.initPoolTx(newPoolInitInfo).buildAndExecute();
await toTx(ctx, WhirlpoolIx.initializePoolIx(ctx.program, newPoolInitInfo)).buildAndExecute();
const whirlpool_1 = await client.getPool(whirlpoolPda.publicKey);
const whirlpool_1 = (await fetcher.getPool(whirlpoolPda.publicKey)) as WhirlpoolData;
assert.equal(whirlpool_1.protocolFeeRate, newDefaultProtocolFeeRate);
});
it("fails when default fee rate exceeds max", async () => {
const { configInitInfo, configKeypairs } = await initTestPool(client, TickSpacing.Standard);
const whirlpoolsConfigKey = configInitInfo.whirlpoolConfigKeypair.publicKey;
const { configInitInfo, configKeypairs } = await initTestPool(ctx, TickSpacing.Standard);
const whirlpoolsConfigKey = configInitInfo.whirlpoolsConfigKeypair.publicKey;
const feeAuthorityKeypair = configKeypairs.feeAuthorityKeypair;
const newDefaultProtocolFeeRate = 20_000;
await assert.rejects(
client
.setDefaultProtocolFeeRateIx({
toTx(
ctx,
WhirlpoolIx.setDefaultProtocolFeeRateIx(ctx.program, {
whirlpoolsConfig: whirlpoolsConfigKey,
feeAuthority: feeAuthorityKeypair.publicKey,
defaultProtocolFeeRate: newDefaultProtocolFeeRate,
})
)
.addSigner(feeAuthorityKeypair)
.buildAndExecute(),
/0x178d/ // ProtocolFeeRateMaxExceeded
@ -88,8 +98,8 @@ describe("set_default_protocol_fee_rate", () => {
});
it("fails when fee authority is not a signer", async () => {
const { configInitInfo, configKeypairs } = await initTestPool(client, TickSpacing.Standard);
const whirlpoolsConfigKey = configInitInfo.whirlpoolConfigKeypair.publicKey;
const { configInitInfo, configKeypairs } = await initTestPool(ctx, TickSpacing.Standard);
const whirlpoolsConfigKey = configInitInfo.whirlpoolsConfigKeypair.publicKey;
const feeAuthorityKeypair = configKeypairs.feeAuthorityKeypair;
const newDefaultProtocolFeeRate = 1000;
@ -105,8 +115,8 @@ describe("set_default_protocol_fee_rate", () => {
});
it("fails when invalid fee authority provided", async () => {
const { configInitInfo } = await initTestPool(client, TickSpacing.Standard);
const whirlpoolsConfigKey = configInitInfo.whirlpoolConfigKeypair.publicKey;
const { configInitInfo } = await initTestPool(ctx, TickSpacing.Standard);
const whirlpoolsConfigKey = configInitInfo.whirlpoolsConfigKeypair.publicKey;
const fakeFeeAuthorityKeypair = anchor.web3.Keypair.generate();
const newDefaultProtocolFeeRate = 1000;

View File

@ -0,0 +1,73 @@
import * as assert from "assert";
import * as anchor from "@project-serum/anchor";
import { WhirlpoolContext, AccountFetcher, WhirlpoolsConfigData, WhirlpoolIx } from "../../src";
import { generateDefaultConfigParams } from "../utils/test-builders";
import { toTx } from "../../src/utils/instructions-util";
describe("set_fee_authority", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
it("successfully set_fee_authority", async () => {
const {
configInitInfo,
configKeypairs: { feeAuthorityKeypair },
} = generateDefaultConfigParams(ctx);
await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configInitInfo)).buildAndExecute();
const newAuthorityKeypair = anchor.web3.Keypair.generate();
await toTx(
ctx,
WhirlpoolIx.setFeeAuthorityIx(ctx.program, {
whirlpoolsConfig: configInitInfo.whirlpoolsConfigKeypair.publicKey,
feeAuthority: feeAuthorityKeypair.publicKey,
newFeeAuthority: newAuthorityKeypair.publicKey,
})
)
.addSigner(feeAuthorityKeypair)
.buildAndExecute();
const config = (await fetcher.getConfig(
configInitInfo.whirlpoolsConfigKeypair.publicKey
)) as WhirlpoolsConfigData;
assert.ok(config.feeAuthority.equals(newAuthorityKeypair.publicKey));
});
it("fails if current fee_authority is not a signer", async () => {
const {
configInitInfo,
configKeypairs: { feeAuthorityKeypair },
} = generateDefaultConfigParams(ctx);
await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configInitInfo)).buildAndExecute();
await assert.rejects(
toTx(
ctx,
WhirlpoolIx.setFeeAuthorityIx(ctx.program, {
whirlpoolsConfig: configInitInfo.whirlpoolsConfigKeypair.publicKey,
feeAuthority: feeAuthorityKeypair.publicKey,
newFeeAuthority: provider.wallet.publicKey,
})
).buildAndExecute(),
/Signature verification failed/
);
});
it("fails if invalid fee_authority provided", async () => {
const { configInitInfo } = generateDefaultConfigParams(ctx);
await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configInitInfo)).buildAndExecute();
await assert.rejects(
toTx(
ctx,
WhirlpoolIx.setFeeAuthorityIx(ctx.program, {
whirlpoolsConfig: configInitInfo.whirlpoolsConfigKeypair.publicKey,
feeAuthority: provider.wallet.publicKey,
newFeeAuthority: provider.wallet.publicKey,
})
).buildAndExecute(),
/0x7dc/ // An address constraint was violated
);
});
});

View File

@ -1,31 +1,30 @@
import * as assert from "assert";
import * as anchor from "@project-serum/anchor";
import { WhirlpoolContext } from "../src/context";
import { WhirlpoolClient } from "../src/client";
import { initTestPool } from "./utils/init-utils";
import { generateDefaultConfigParams } from "./utils/test-builders";
import { TickSpacing } from "./utils";
import { WhirlpoolContext, AccountFetcher, WhirlpoolData, WhirlpoolIx } from "../../src";
import { TickSpacing } from "../utils";
import { initTestPool } from "../utils/init-utils";
import { generateDefaultConfigParams } from "../utils/test-builders";
import { toTx } from "../../src/utils/instructions-util";
describe("set_fee_rate", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
it("successfully sets_fee_rate", async () => {
const { poolInitInfo, configInitInfo, configKeypairs, feeTierParams } = await initTestPool(
client,
ctx,
TickSpacing.Standard
);
const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey;
const whirlpoolsConfigKey = configInitInfo.whirlpoolConfigKeypair.publicKey;
const whirlpoolsConfigKey = configInitInfo.whirlpoolsConfigKeypair.publicKey;
const feeAuthorityKeypair = configKeypairs.feeAuthorityKeypair;
const newFeeRate = 50;
let whirlpool = await client.getPool(whirlpoolKey);
let whirlpool = (await fetcher.getPool(whirlpoolKey, true)) as WhirlpoolData;
assert.equal(whirlpool.feeRate, feeTierParams.defaultFeeRate);
@ -38,28 +37,30 @@ describe("set_fee_rate", () => {
signers: [feeAuthorityKeypair],
});
whirlpool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey);
whirlpool = (await fetcher.getPool(poolInitInfo.whirlpoolPda.publicKey, true)) as WhirlpoolData;
assert.equal(whirlpool.feeRate, newFeeRate);
});
it("fails when fee rate exceeds max", async () => {
const { poolInitInfo, configInitInfo, configKeypairs } = await initTestPool(
client,
ctx,
TickSpacing.Standard
);
const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey;
const whirlpoolsConfigKey = configInitInfo.whirlpoolConfigKeypair.publicKey;
const whirlpoolsConfigKey = configInitInfo.whirlpoolsConfigKeypair.publicKey;
const feeAuthorityKeypair = configKeypairs.feeAuthorityKeypair;
const newFeeRate = 20_000;
await assert.rejects(
client
.setFeeRateIx({
toTx(
ctx,
WhirlpoolIx.setFeeRateIx(ctx.program, {
whirlpoolsConfig: whirlpoolsConfigKey,
whirlpool: whirlpoolKey,
feeAuthority: feeAuthorityKeypair.publicKey,
feeRate: newFeeRate,
})
)
.addSigner(configKeypairs.feeAuthorityKeypair)
.buildAndExecute(),
/0x178c/ // FeeRateMaxExceeded
@ -68,43 +69,47 @@ describe("set_fee_rate", () => {
it("fails when fee authority is not signer", async () => {
const { poolInitInfo, configInitInfo, configKeypairs } = await initTestPool(
client,
ctx,
TickSpacing.Standard
);
const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey;
const whirlpoolsConfigKey = configInitInfo.whirlpoolConfigKeypair.publicKey;
const whirlpoolsConfigKey = configInitInfo.whirlpoolsConfigKeypair.publicKey;
const feeAuthorityKeypair = configKeypairs.feeAuthorityKeypair;
const newFeeRate = 1000;
await assert.rejects(
client
.setFeeRateIx({
toTx(
ctx,
WhirlpoolIx.setFeeRateIx(ctx.program, {
whirlpoolsConfig: whirlpoolsConfigKey,
whirlpool: whirlpoolKey,
feeAuthority: feeAuthorityKeypair.publicKey,
feeRate: newFeeRate,
})
.buildAndExecute(),
).buildAndExecute(),
/Signature verification failed/
);
});
it("fails when whirlpool and whirlpools config don't match", async () => {
const { poolInitInfo, configInitInfo, configKeypairs } = await initTestPool(
client,
ctx,
TickSpacing.Standard
);
const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey;
const feeAuthorityKeypair = configKeypairs.feeAuthorityKeypair;
const { configInitInfo: otherConfigInitInfo } = generateDefaultConfigParams(context);
await client.initConfigTx(otherConfigInitInfo).buildAndExecute();
const { configInitInfo: otherConfigInitInfo } = generateDefaultConfigParams(ctx);
await toTx(
ctx,
WhirlpoolIx.initializeConfigIx(ctx.program, otherConfigInitInfo)
).buildAndExecute();
const newFeeRate = 1000;
await assert.rejects(
context.program.rpc.setFeeRate(newFeeRate, {
ctx.program.rpc.setFeeRate(newFeeRate, {
accounts: {
whirlpoolsConfig: otherConfigInitInfo.whirlpoolConfigKeypair.publicKey,
whirlpoolsConfig: otherConfigInitInfo.whirlpoolsConfigKeypair.publicKey,
whirlpool: whirlpoolKey,
feeAuthority: feeAuthorityKeypair.publicKey,
},
@ -115,16 +120,16 @@ describe("set_fee_rate", () => {
});
it("fails when fee authority is invalid", async () => {
const { poolInitInfo, configInitInfo } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo, configInitInfo } = await initTestPool(ctx, TickSpacing.Standard);
const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey;
const fakeAuthorityKeypair = anchor.web3.Keypair.generate();
const newFeeRate = 1000;
await assert.rejects(
context.program.rpc.setFeeRate(newFeeRate, {
ctx.program.rpc.setFeeRate(newFeeRate, {
accounts: {
whirlpoolsConfig: configInitInfo.whirlpoolConfigKeypair.publicKey,
whirlpoolsConfig: configInitInfo.whirlpoolsConfigKeypair.publicKey,
whirlpool: whirlpoolKey,
feeAuthority: fakeAuthorityKeypair.publicKey,
},

View File

@ -1,30 +1,30 @@
import * as assert from "assert";
import * as anchor from "@project-serum/anchor";
import { WhirlpoolContext } from "../src/context";
import { WhirlpoolClient } from "../src/client";
import { initTestPool } from "./utils/init-utils";
import { generateDefaultConfigParams } from "./utils/test-builders";
import { TickSpacing } from "./utils";
import { WhirlpoolContext, AccountFetcher, WhirlpoolData, WhirlpoolIx } from "../../src";
import { TickSpacing } from "../utils";
import { initTestPool } from "../utils/init-utils";
import { generateDefaultConfigParams } from "../utils/test-builders";
import { toTx } from "../../src/utils/instructions-util";
describe("set_protocol_fee_rate", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
it("successfully sets_protocol_fee_rate", async () => {
const { poolInitInfo, configInitInfo, configKeypairs } = await initTestPool(
client,
ctx,
TickSpacing.Standard
);
const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey;
const whirlpoolsConfigKey = configInitInfo.whirlpoolConfigKeypair.publicKey;
const whirlpoolsConfigKey = configInitInfo.whirlpoolsConfigKeypair.publicKey;
const feeAuthorityKeypair = configKeypairs.feeAuthorityKeypair;
const newProtocolFeeRate = 50;
let whirlpool = await client.getPool(whirlpoolKey);
let whirlpool = (await fetcher.getPool(whirlpoolKey, true)) as WhirlpoolData;
assert.equal(whirlpool.protocolFeeRate, configInitInfo.defaultProtocolFeeRate);
@ -37,28 +37,30 @@ describe("set_protocol_fee_rate", () => {
signers: [feeAuthorityKeypair],
});
whirlpool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey);
whirlpool = (await fetcher.getPool(poolInitInfo.whirlpoolPda.publicKey, true)) as WhirlpoolData;
assert.equal(whirlpool.protocolFeeRate, newProtocolFeeRate);
});
it("fails when protocol fee rate exceeds max", async () => {
const { poolInitInfo, configInitInfo, configKeypairs } = await initTestPool(
client,
ctx,
TickSpacing.Standard
);
const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey;
const whirlpoolsConfigKey = configInitInfo.whirlpoolConfigKeypair.publicKey;
const whirlpoolsConfigKey = configInitInfo.whirlpoolsConfigKeypair.publicKey;
const feeAuthorityKeypair = configKeypairs.feeAuthorityKeypair;
const newProtocolFeeRate = 3_000;
await assert.rejects(
client
.setProtocolFeeRateIx({
toTx(
ctx,
WhirlpoolIx.setProtocolFeeRateIx(ctx.program, {
whirlpoolsConfig: whirlpoolsConfigKey,
whirlpool: whirlpoolKey,
feeAuthority: feeAuthorityKeypair.publicKey,
protocolFeeRate: newProtocolFeeRate,
})
)
.addSigner(configKeypairs.feeAuthorityKeypair)
.buildAndExecute(),
/0x178d/ // ProtocolFeeRateMaxExceeded
@ -67,40 +69,44 @@ describe("set_protocol_fee_rate", () => {
it("fails when fee authority is not signer", async () => {
const { poolInitInfo, configInitInfo, configKeypairs } = await initTestPool(
client,
ctx,
TickSpacing.Standard
);
const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey;
const whirlpoolsConfigKey = configInitInfo.whirlpoolConfigKeypair.publicKey;
const whirlpoolsConfigKey = configInitInfo.whirlpoolsConfigKeypair.publicKey;
const feeAuthorityKeypair = configKeypairs.feeAuthorityKeypair;
const newProtocolFeeRate = 1000;
await assert.rejects(
client
.setProtocolFeeRateIx({
toTx(
ctx,
WhirlpoolIx.setProtocolFeeRateIx(ctx.program, {
whirlpoolsConfig: whirlpoolsConfigKey,
whirlpool: whirlpoolKey,
feeAuthority: feeAuthorityKeypair.publicKey,
protocolFeeRate: newProtocolFeeRate,
})
.buildAndExecute(),
).buildAndExecute(),
/Signature verification failed/
);
});
it("fails when whirlpool and whirlpools config don't match", async () => {
const { poolInitInfo, configKeypairs } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo, configKeypairs } = await initTestPool(ctx, TickSpacing.Standard);
const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey;
const feeAuthorityKeypair = configKeypairs.feeAuthorityKeypair;
const { configInitInfo: otherConfigInitInfo } = generateDefaultConfigParams(context);
await client.initConfigTx(otherConfigInitInfo).buildAndExecute();
const { configInitInfo: otherConfigInitInfo } = generateDefaultConfigParams(ctx);
await toTx(
ctx,
WhirlpoolIx.initializeConfigIx(ctx.program, otherConfigInitInfo)
).buildAndExecute();
const newProtocolFeeRate = 1000;
await assert.rejects(
context.program.rpc.setProtocolFeeRate(newProtocolFeeRate, {
ctx.program.rpc.setProtocolFeeRate(newProtocolFeeRate, {
accounts: {
whirlpoolsConfig: otherConfigInitInfo.whirlpoolConfigKeypair.publicKey,
whirlpoolsConfig: otherConfigInitInfo.whirlpoolsConfigKeypair.publicKey,
whirlpool: whirlpoolKey,
feeAuthority: feeAuthorityKeypair.publicKey,
},
@ -111,15 +117,15 @@ describe("set_protocol_fee_rate", () => {
});
it("fails when fee authority is invalid", async () => {
const { poolInitInfo, configInitInfo } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo, configInitInfo } = await initTestPool(ctx, TickSpacing.Standard);
const whirlpoolKey = poolInitInfo.whirlpoolPda.publicKey;
const fakeAuthorityKeypair = anchor.web3.Keypair.generate();
const newProtocolFeeRate = 1000;
await assert.rejects(
context.program.rpc.setProtocolFeeRate(newProtocolFeeRate, {
ctx.program.rpc.setProtocolFeeRate(newProtocolFeeRate, {
accounts: {
whirlpoolsConfig: configInitInfo.whirlpoolConfigKeypair.publicKey,
whirlpoolsConfig: configInitInfo.whirlpoolsConfigKeypair.publicKey,
whirlpool: whirlpoolKey,
feeAuthority: fakeAuthorityKeypair.publicKey,
},

View File

@ -1,28 +1,32 @@
import * as assert from "assert";
import * as anchor from "@project-serum/anchor";
import { WhirlpoolContext } from "../src/context";
import { WhirlpoolClient } from "../src/client";
import { NUM_REWARDS } from "../src/types/public";
import { TransactionBuilder } from "../src/utils/transactions/transactions-builder";
import { buildSetRewardAuthorityIx } from "../src/instructions/set-reward-authority-ix";
import { initTestPool } from "./utils/init-utils";
import { TickSpacing } from "./utils";
import {
WhirlpoolContext,
AccountFetcher,
NUM_REWARDS,
WhirlpoolData,
WhirlpoolIx,
} from "../../src";
import { TickSpacing } from "../utils";
import { initTestPool } from "../utils/init-utils";
import { TransactionBuilder } from "@orca-so/common-sdk";
import { toTx } from "../../src/utils/instructions-util";
describe("set_reward_authority", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
it("successfully set_reward_authority at every reward index", async () => {
const { configKeypairs, poolInitInfo } = await initTestPool(client, TickSpacing.Standard);
const { configKeypairs, poolInitInfo } = await initTestPool(ctx, TickSpacing.Standard);
const newKeypairs = generateKeypairs(NUM_REWARDS);
const txBuilder = new TransactionBuilder(provider);
for (let i = 0; i < NUM_REWARDS; i++) {
txBuilder.addInstruction(
buildSetRewardAuthorityIx(context, {
WhirlpoolIx.setRewardAuthorityIx(ctx.program, {
whirlpool: poolInitInfo.whirlpoolPda.publicKey,
rewardAuthority: configKeypairs.rewardEmissionsSuperAuthorityKeypair.publicKey,
newRewardAuthority: newKeypairs[i].publicKey,
@ -34,25 +38,27 @@ describe("set_reward_authority", () => {
.addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair)
.buildAndExecute();
const pool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey);
const pool = (await fetcher.getPool(poolInitInfo.whirlpoolPda.publicKey)) as WhirlpoolData;
for (let i = 0; i < NUM_REWARDS; i++) {
assert.ok(pool.rewardInfos[i].authority.equals(newKeypairs[i].publicKey));
}
});
it("fails when provided reward_authority does not match whirlpool reward authority", async () => {
const { poolInitInfo } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo } = await initTestPool(ctx, TickSpacing.Standard);
const fakeAuthority = anchor.web3.Keypair.generate();
const newAuthority = anchor.web3.Keypair.generate();
await assert.rejects(
client
.setRewardAuthorityTx({
toTx(
ctx,
WhirlpoolIx.setRewardAuthorityIx(ctx.program, {
whirlpool: poolInitInfo.whirlpoolPda.publicKey,
rewardAuthority: fakeAuthority.publicKey,
newRewardAuthority: newAuthority.publicKey,
rewardIndex: 0,
})
)
.addSigner(fakeAuthority)
.buildAndExecute(),
/0x7dc/ // An address constraint was violated
@ -60,26 +66,31 @@ describe("set_reward_authority", () => {
});
it("fails on invalid reward index", async () => {
const { configKeypairs, poolInitInfo } = await initTestPool(client, TickSpacing.Standard);
const { configKeypairs, poolInitInfo } = await initTestPool(ctx, TickSpacing.Standard);
const newAuthority = anchor.web3.Keypair.generate();
assert.throws(() => {
client.setRewardAuthorityTx({
whirlpool: poolInitInfo.whirlpoolPda.publicKey,
rewardAuthority: configKeypairs.rewardEmissionsSuperAuthorityKeypair.publicKey,
newRewardAuthority: newAuthority.publicKey,
rewardIndex: -1,
});
toTx(
ctx,
WhirlpoolIx.setRewardAuthorityIx(ctx.program, {
whirlpool: poolInitInfo.whirlpoolPda.publicKey,
rewardAuthority: configKeypairs.rewardEmissionsSuperAuthorityKeypair.publicKey,
newRewardAuthority: newAuthority.publicKey,
rewardIndex: -1,
})
).buildAndExecute();
}, /out of range/);
await assert.rejects(
client
.setRewardAuthorityTx({
toTx(
ctx,
WhirlpoolIx.setRewardAuthorityIx(ctx.program, {
whirlpool: poolInitInfo.whirlpoolPda.publicKey,
rewardAuthority: configKeypairs.rewardEmissionsSuperAuthorityKeypair.publicKey,
newRewardAuthority: newAuthority.publicKey,
rewardIndex: 255,
})
)
.addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair)
.buildAndExecute()
// /failed to send transaction/
@ -87,18 +98,19 @@ describe("set_reward_authority", () => {
});
it("fails when reward_authority is not a signer", async () => {
const { configKeypairs, poolInitInfo } = await initTestPool(client, TickSpacing.Standard);
const { configKeypairs, poolInitInfo } = await initTestPool(ctx, TickSpacing.Standard);
const newAuthority = anchor.web3.Keypair.generate();
await assert.rejects(
client
.setRewardAuthorityTx({
toTx(
ctx,
WhirlpoolIx.setRewardAuthorityIx(ctx.program, {
whirlpool: poolInitInfo.whirlpoolPda.publicKey,
rewardAuthority: configKeypairs.rewardEmissionsSuperAuthorityKeypair.publicKey,
newRewardAuthority: newAuthority.publicKey,
rewardIndex: 0,
})
.buildAndExecute(),
).buildAndExecute(),
/Signature verification failed/
);
});

View File

@ -1,54 +1,58 @@
import * as assert from "assert";
import * as anchor from "@project-serum/anchor";
import { WhirlpoolContext } from "../src/context";
import { WhirlpoolClient } from "../src/client";
import { initTestPool } from "./utils/init-utils";
import { TickSpacing } from "./utils";
import { WhirlpoolContext, AccountFetcher, WhirlpoolData, WhirlpoolIx } from "../../src";
import { TickSpacing } from "../utils";
import { initTestPool } from "../utils/init-utils";
import { toTx } from "../../src/utils/instructions-util";
describe("set_reward_authority_by_super_authority", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
it("successfully set_reward_authority_by_super_authority", async () => {
const { configKeypairs, poolInitInfo, configInitInfo } = await initTestPool(
client,
ctx,
TickSpacing.Standard
);
const newAuthorityKeypair = anchor.web3.Keypair.generate();
await client
.setRewardAuthorityBySuperAuthorityTx({
whirlpoolsConfig: configInitInfo.whirlpoolConfigKeypair.publicKey,
await toTx(
ctx,
WhirlpoolIx.setRewardAuthorityBySuperAuthorityIx(ctx.program, {
whirlpoolsConfig: configInitInfo.whirlpoolsConfigKeypair.publicKey,
whirlpool: poolInitInfo.whirlpoolPda.publicKey,
rewardEmissionsSuperAuthority:
configKeypairs.rewardEmissionsSuperAuthorityKeypair.publicKey,
newRewardAuthority: newAuthorityKeypair.publicKey,
rewardIndex: 0,
})
)
.addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair)
.buildAndExecute();
const pool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey);
const pool = (await fetcher.getPool(poolInitInfo.whirlpoolPda.publicKey)) as WhirlpoolData;
assert.ok(pool.rewardInfos[0].authority.equals(newAuthorityKeypair.publicKey));
});
it("fails if invalid whirlpool provided", async () => {
const { configKeypairs, configInitInfo } = await initTestPool(client, TickSpacing.Standard);
const { configKeypairs, configInitInfo } = await initTestPool(ctx, TickSpacing.Standard);
const {
poolInitInfo: { whirlpoolPda: invalidPool },
} = await initTestPool(client, TickSpacing.Standard);
} = await initTestPool(ctx, TickSpacing.Standard);
await assert.rejects(
client
.setRewardAuthorityBySuperAuthorityTx({
whirlpoolsConfig: configInitInfo.whirlpoolConfigKeypair.publicKey,
toTx(
ctx,
WhirlpoolIx.setRewardAuthorityBySuperAuthorityIx(ctx.program, {
whirlpoolsConfig: configInitInfo.whirlpoolsConfigKeypair.publicKey,
whirlpool: invalidPool.publicKey,
rewardEmissionsSuperAuthority:
configKeypairs.rewardEmissionsSuperAuthorityKeypair.publicKey,
newRewardAuthority: provider.wallet.publicKey,
rewardIndex: 0,
})
)
.addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair)
.buildAndExecute(),
/0x7d1/ // A has_one constraint was violated
@ -56,18 +60,20 @@ describe("set_reward_authority_by_super_authority", () => {
});
it("fails if invalid super authority provided", async () => {
const { poolInitInfo, configInitInfo } = await initTestPool(client, TickSpacing.Standard);
const { poolInitInfo, configInitInfo } = await initTestPool(ctx, TickSpacing.Standard);
const invalidSuperAuthorityKeypair = anchor.web3.Keypair.generate();
await assert.rejects(
client
.setRewardAuthorityBySuperAuthorityTx({
whirlpoolsConfig: configInitInfo.whirlpoolConfigKeypair.publicKey,
toTx(
ctx,
WhirlpoolIx.setRewardAuthorityBySuperAuthorityIx(ctx.program, {
whirlpoolsConfig: configInitInfo.whirlpoolsConfigKeypair.publicKey,
whirlpool: poolInitInfo.whirlpoolPda.publicKey,
rewardEmissionsSuperAuthority: invalidSuperAuthorityKeypair.publicKey,
newRewardAuthority: provider.wallet.publicKey,
rewardIndex: 0,
})
)
.addSigner(invalidSuperAuthorityKeypair)
.buildAndExecute(),
/0x7dc/ // An address constraint was violated
@ -76,55 +82,60 @@ describe("set_reward_authority_by_super_authority", () => {
it("fails if super authority is not a signer", async () => {
const { configKeypairs, poolInitInfo, configInitInfo } = await initTestPool(
client,
ctx,
TickSpacing.Standard
);
await assert.rejects(
client
.setRewardAuthorityBySuperAuthorityTx({
whirlpoolsConfig: configInitInfo.whirlpoolConfigKeypair.publicKey,
toTx(
ctx,
WhirlpoolIx.setRewardAuthorityBySuperAuthorityIx(ctx.program, {
whirlpoolsConfig: configInitInfo.whirlpoolsConfigKeypair.publicKey,
whirlpool: poolInitInfo.whirlpoolPda.publicKey,
rewardEmissionsSuperAuthority:
configKeypairs.rewardEmissionsSuperAuthorityKeypair.publicKey,
newRewardAuthority: provider.wallet.publicKey,
rewardIndex: 0,
})
.buildAndExecute(),
).buildAndExecute(),
/Signature verification failed/
);
});
it("fails on invalid reward index", async () => {
const { configKeypairs, poolInitInfo, configInitInfo } = await initTestPool(
client,
ctx,
TickSpacing.Standard
);
assert.throws(() => {
client
.setRewardAuthorityBySuperAuthorityTx({
whirlpoolsConfig: configInitInfo.whirlpoolConfigKeypair.publicKey,
toTx(
ctx,
WhirlpoolIx.setRewardAuthorityBySuperAuthorityIx(ctx.program, {
whirlpoolsConfig: configInitInfo.whirlpoolsConfigKeypair.publicKey,
whirlpool: poolInitInfo.whirlpoolPda.publicKey,
rewardEmissionsSuperAuthority:
configKeypairs.rewardEmissionsSuperAuthorityKeypair.publicKey,
newRewardAuthority: provider.wallet.publicKey,
rewardIndex: -1,
})
)
.addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair)
.buildAndExecute();
}, /out of range/);
await assert.rejects(
client
.setRewardAuthorityBySuperAuthorityTx({
whirlpoolsConfig: configInitInfo.whirlpoolConfigKeypair.publicKey,
toTx(
ctx,
WhirlpoolIx.setRewardAuthorityBySuperAuthorityIx(ctx.program, {
whirlpoolsConfig: configInitInfo.whirlpoolsConfigKeypair.publicKey,
whirlpool: poolInitInfo.whirlpoolPda.publicKey,
rewardEmissionsSuperAuthority:
configKeypairs.rewardEmissionsSuperAuthorityKeypair.publicKey,
newRewardAuthority: provider.wallet.publicKey,
rewardIndex: 200,
})
)
.addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair)
.buildAndExecute(),
/0x178a/ // InvalidRewardIndex

View File

@ -1,22 +1,22 @@
import * as anchor from "@project-serum/anchor";
import * as assert from "assert";
import { WhirlpoolClient } from "../src/client";
import { WhirlpoolContext } from "../src/context";
import { createAndMintToTokenAccount, mintToByAuthority, TickSpacing, ZERO_BN } from "./utils";
import { initializeReward, initTestPool } from "./utils/init-utils";
import { WhirlpoolContext, AccountFetcher, WhirlpoolData, WhirlpoolIx } from "../../src";
import { toTx } from "../../src/utils/instructions-util";
import { TickSpacing, mintToByAuthority, ZERO_BN, createAndMintToTokenAccount } from "../utils";
import { initTestPool, initializeReward } from "../utils/init-utils";
describe("set_reward_emissions", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
const emissionsPerSecondX64 = new anchor.BN(10_000).shln(64).div(new anchor.BN(60 * 60 * 24));
it("successfully set_reward_emissions", async () => {
const { poolInitInfo, configInitInfo, configKeypairs } = await initTestPool(
client,
ctx,
TickSpacing.Standard
);
@ -25,7 +25,7 @@ describe("set_reward_emissions", () => {
const {
params: { rewardVaultKeypair, rewardMint },
} = await initializeReward(
client,
ctx,
configKeypairs.rewardEmissionsSuperAuthorityKeypair,
poolInitInfo.whirlpoolPda.publicKey,
rewardIndex
@ -33,39 +33,46 @@ describe("set_reward_emissions", () => {
await mintToByAuthority(provider, rewardMint, rewardVaultKeypair.publicKey, 10000);
await client
.setRewardEmissionsTx({
await toTx(
ctx,
WhirlpoolIx.setRewardEmissionsIx(ctx.program, {
rewardAuthority: configInitInfo.rewardEmissionsSuperAuthority,
whirlpool: poolInitInfo.whirlpoolPda.publicKey,
rewardIndex,
rewardVault: rewardVaultKeypair.publicKey,
rewardVaultKey: rewardVaultKeypair.publicKey,
emissionsPerSecondX64,
})
)
.addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair)
.buildAndExecute();
let whirlpool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey);
let whirlpool = (await fetcher.getPool(
poolInitInfo.whirlpoolPda.publicKey,
true
)) as WhirlpoolData;
assert.ok(whirlpool.rewardInfos[0].emissionsPerSecondX64.eq(emissionsPerSecondX64));
// Successfuly set emissions back to zero
await client
.setRewardEmissionsTx({
await toTx(
ctx,
WhirlpoolIx.setRewardEmissionsIx(ctx.program, {
rewardAuthority: configInitInfo.rewardEmissionsSuperAuthority,
whirlpool: poolInitInfo.whirlpoolPda.publicKey,
rewardIndex,
rewardVault: rewardVaultKeypair.publicKey,
rewardVaultKey: rewardVaultKeypair.publicKey,
emissionsPerSecondX64: ZERO_BN,
})
)
.addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair)
.buildAndExecute();
whirlpool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey);
whirlpool = (await fetcher.getPool(poolInitInfo.whirlpoolPda.publicKey, true)) as WhirlpoolData;
assert.ok(whirlpool.rewardInfos[0].emissionsPerSecondX64.eq(ZERO_BN));
});
it("fails when token vault does not contain at least 1 day of emission runway", async () => {
const { poolInitInfo, configInitInfo, configKeypairs } = await initTestPool(
client,
ctx,
TickSpacing.Standard
);
@ -74,21 +81,23 @@ describe("set_reward_emissions", () => {
const {
params: { rewardVaultKeypair },
} = await initializeReward(
client,
ctx,
configKeypairs.rewardEmissionsSuperAuthorityKeypair,
poolInitInfo.whirlpoolPda.publicKey,
rewardIndex
);
await assert.rejects(
client
.setRewardEmissionsTx({
toTx(
ctx,
WhirlpoolIx.setRewardEmissionsIx(ctx.program, {
rewardAuthority: configInitInfo.rewardEmissionsSuperAuthority,
whirlpool: poolInitInfo.whirlpoolPda.publicKey,
rewardIndex,
rewardVault: rewardVaultKeypair.publicKey,
rewardVaultKey: rewardVaultKeypair.publicKey,
emissionsPerSecondX64,
})
)
.addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair)
.buildAndExecute(),
/0x178b/ // RewardVaultAmountInsufficient
@ -97,7 +106,7 @@ describe("set_reward_emissions", () => {
it("fails if provided reward vault does not match whirlpool reward vault", async () => {
const { poolInitInfo, configInitInfo, configKeypairs } = await initTestPool(
client,
ctx,
TickSpacing.Standard
);
@ -105,7 +114,7 @@ describe("set_reward_emissions", () => {
const {
params: { rewardVaultKeypair, rewardMint },
} = await initializeReward(
client,
ctx,
configKeypairs.rewardEmissionsSuperAuthorityKeypair,
poolInitInfo.whirlpoolPda.publicKey,
rewardIndex
@ -114,14 +123,16 @@ describe("set_reward_emissions", () => {
const fakeVault = await createAndMintToTokenAccount(provider, rewardMint, 10000);
await assert.rejects(
client
.setRewardEmissionsTx({
toTx(
ctx,
WhirlpoolIx.setRewardEmissionsIx(ctx.program, {
whirlpool: poolInitInfo.whirlpoolPda.publicKey,
rewardAuthority: configInitInfo.rewardEmissionsSuperAuthority,
rewardVault: fakeVault,
rewardVaultKey: fakeVault,
rewardIndex,
emissionsPerSecondX64,
})
)
.addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair)
.buildAndExecute(),
/0x7dc/ // An address constraint was violated
@ -130,21 +141,23 @@ describe("set_reward_emissions", () => {
it("cannot set emission for an uninitialized reward", async () => {
const { poolInitInfo, configInitInfo, configKeypairs } = await initTestPool(
client,
ctx,
TickSpacing.Standard
);
const rewardIndex = 0;
await assert.rejects(
client
.setRewardEmissionsTx({
toTx(
ctx,
WhirlpoolIx.setRewardEmissionsIx(ctx.program, {
whirlpool: poolInitInfo.whirlpoolPda.publicKey,
rewardAuthority: configInitInfo.rewardEmissionsSuperAuthority,
rewardVault: anchor.web3.PublicKey.default,
rewardVaultKey: anchor.web3.PublicKey.default,
rewardIndex: rewardIndex,
emissionsPerSecondX64,
})
)
.addSigner(configKeypairs.rewardEmissionsSuperAuthorityKeypair)
.buildAndExecute(),
/0xbbf/ // AccountOwnedByWrongProgram
@ -153,29 +166,30 @@ describe("set_reward_emissions", () => {
it("cannot set emission without the authority's signature", async () => {
const { poolInitInfo, configInitInfo, configKeypairs } = await initTestPool(
client,
ctx,
TickSpacing.Standard
);
const rewardIndex = 0;
await initializeReward(
client,
ctx,
configKeypairs.rewardEmissionsSuperAuthorityKeypair,
poolInitInfo.whirlpoolPda.publicKey,
rewardIndex
);
await assert.rejects(
client
.setRewardEmissionsTx({
toTx(
ctx,
WhirlpoolIx.setRewardEmissionsIx(ctx.program, {
rewardAuthority: configInitInfo.rewardEmissionsSuperAuthority,
whirlpool: poolInitInfo.whirlpoolPda.publicKey,
rewardIndex,
rewardVault: provider.wallet.publicKey, // TODO fix
rewardVaultKey: provider.wallet.publicKey, // TODO fix
emissionsPerSecondX64,
})
.buildAndExecute(),
).buildAndExecute(),
/Signature verification failed/
);
});

View File

@ -1,35 +1,39 @@
import * as assert from "assert";
import * as anchor from "@project-serum/anchor";
import { WhirlpoolContext } from "../src/context";
import { WhirlpoolClient } from "../src/client";
import { generateDefaultConfigParams } from "./utils/test-builders";
import { WhirlpoolContext, AccountFetcher, WhirlpoolsConfigData, WhirlpoolIx } from "../../src";
import { generateDefaultConfigParams } from "../utils/test-builders";
import { toTx } from "../../src/utils/instructions-util";
describe("set_reward_emissions_super_authority", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
it("successfully set_reward_emissions_super_authority with super authority keypair", async () => {
const {
configInitInfo,
configKeypairs: { rewardEmissionsSuperAuthorityKeypair },
} = generateDefaultConfigParams(context);
} = generateDefaultConfigParams(ctx);
await client.initConfigTx(configInitInfo).buildAndExecute();
await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configInitInfo)).buildAndExecute();
const newAuthorityKeypair = anchor.web3.Keypair.generate();
await client
.setRewardEmissionsSuperAuthorityTx({
whirlpoolsConfig: configInitInfo.whirlpoolConfigKeypair.publicKey,
await toTx(
ctx,
WhirlpoolIx.setRewardEmissionsSuperAuthorityIx(ctx.program, {
whirlpoolsConfig: configInitInfo.whirlpoolsConfigKeypair.publicKey,
rewardEmissionsSuperAuthority: rewardEmissionsSuperAuthorityKeypair.publicKey,
newRewardEmissionsSuperAuthority: newAuthorityKeypair.publicKey,
})
)
.addSigner(rewardEmissionsSuperAuthorityKeypair)
.buildAndExecute();
const config = await client.getConfig(configInitInfo.whirlpoolConfigKeypair.publicKey);
const config = (await fetcher.getConfig(
configInitInfo.whirlpoolsConfigKeypair.publicKey
)) as WhirlpoolsConfigData;
assert.ok(config.rewardEmissionsSuperAuthority.equals(newAuthorityKeypair.publicKey));
});
@ -37,13 +41,13 @@ describe("set_reward_emissions_super_authority", () => {
const {
configInitInfo,
configKeypairs: { rewardEmissionsSuperAuthorityKeypair },
} = generateDefaultConfigParams(context);
await client.initConfigTx(configInitInfo).buildAndExecute();
} = generateDefaultConfigParams(ctx);
await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configInitInfo)).buildAndExecute();
await assert.rejects(
context.program.rpc.setRewardEmissionsSuperAuthority({
ctx.program.rpc.setRewardEmissionsSuperAuthority({
accounts: {
whirlpoolsConfig: configInitInfo.whirlpoolConfigKeypair.publicKey,
whirlpoolsConfig: configInitInfo.whirlpoolsConfigKeypair.publicKey,
rewardEmissionsSuperAuthority: rewardEmissionsSuperAuthorityKeypair.publicKey,
newRewardEmissionsSuperAuthority: provider.wallet.publicKey,
},
@ -53,17 +57,18 @@ describe("set_reward_emissions_super_authority", () => {
});
it("fails if incorrect reward_emissions_super_authority is passed in", async () => {
const { configInitInfo } = generateDefaultConfigParams(context);
await client.initConfigTx(configInitInfo).buildAndExecute();
const { configInitInfo } = generateDefaultConfigParams(ctx);
await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configInitInfo)).buildAndExecute();
await assert.rejects(
client
.setRewardEmissionsSuperAuthorityTx({
whirlpoolsConfig: configInitInfo.whirlpoolConfigKeypair.publicKey,
toTx(
ctx,
WhirlpoolIx.setRewardEmissionsSuperAuthorityIx(ctx.program, {
whirlpoolsConfig: configInitInfo.whirlpoolsConfigKeypair.publicKey,
rewardEmissionsSuperAuthority: provider.wallet.publicKey,
newRewardEmissionsSuperAuthority: provider.wallet.publicKey,
})
.buildAndExecute(),
).buildAndExecute(),
/0x7dc/ // An address constraint was violated
);
});

File diff suppressed because it is too large Load Diff

View File

@ -2,19 +2,19 @@ import * as anchor from "@project-serum/anchor";
import * as assert from "assert";
import { u64 } from "@solana/spl-token";
import Decimal from "decimal.js";
import { getOraclePda, getTickArrayPda, toX64 } from "../src";
import { WhirlpoolClient } from "../src/client";
import { WhirlpoolContext } from "../src/context";
import { sleep, TickSpacing, ZERO_BN } from "./utils";
import { WhirlpoolTestFixture } from "./utils/fixture";
import { initTestPool } from "./utils/init-utils";
import { WhirlpoolContext, AccountFetcher, PositionData, WhirlpoolIx, PDAUtil } from "../../src";
import { TickSpacing, ZERO_BN, sleep } from "../utils";
import { WhirlpoolTestFixture } from "../utils/fixture";
import { initTestPool } from "../utils/init-utils";
import { MathUtil } from "@orca-so/common-sdk";
import { toTx } from "../../src/utils/instructions-util";
describe("update_fees_and_rewards", () => {
const provider = anchor.Provider.local();
anchor.setProvider(anchor.Provider.env());
const program = anchor.workspace.Whirlpool;
const context = WhirlpoolContext.fromWorkspace(provider, program);
const client = new WhirlpoolClient(context);
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = new AccountFetcher(ctx.connection);
it("successfully updates fees and rewards", async () => {
// In same tick array - start index 22528
@ -22,10 +22,12 @@ describe("update_fees_and_rewards", () => {
const tickUpperIndex = 33536;
const tickSpacing = TickSpacing.Standard;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing,
positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: new u64(1_000_000) }],
rewards: [{ emissionsPerSecondX64: toX64(new Decimal(2)), vaultAmount: new u64(1_000_000) }],
rewards: [
{ emissionsPerSecondX64: MathUtil.toX64(new Decimal(2)), vaultAmount: new u64(1_000_000) },
],
});
const {
poolInitInfo: { whirlpoolPda, tokenVaultAKeypair, tokenVaultBKeypair },
@ -34,25 +36,29 @@ describe("update_fees_and_rewards", () => {
positions,
} = fixture.getInfos();
const tickArrayPda = getTickArrayPda(context.program.programId, whirlpoolPda.publicKey, 22528);
const tickArrayPda = PDAUtil.getTickArray(ctx.program.programId, whirlpoolPda.publicKey, 22528);
const positionBefore = await client.getPosition(positions[0].publicKey);
const positionBefore = (await fetcher.getPosition(
positions[0].publicKey,
true
)) as PositionData;
assert.ok(positionBefore.feeGrowthCheckpointA.eq(ZERO_BN));
assert.ok(positionBefore.feeGrowthCheckpointB.eq(ZERO_BN));
assert.ok(positionBefore.rewardInfos[0].amountOwed.eq(ZERO_BN));
assert.ok(positionBefore.rewardInfos[0].growthInsideCheckpoint.eq(ZERO_BN));
const oraclePda = getOraclePda(client.context.program.programId, whirlpoolPda.publicKey);
const oraclePda = PDAUtil.getOracle(ctx.program.programId, whirlpoolPda.publicKey);
await client
.swapTx({
await toTx(
ctx,
WhirlpoolIx.swapIx(ctx.program, {
amount: new u64(100_000),
otherAmountThreshold: ZERO_BN,
sqrtPriceLimit: toX64(new Decimal(4.95)),
sqrtPriceLimit: MathUtil.toX64(new Decimal(4.95)),
amountSpecifiedIsInput: true,
aToB: true,
whirlpool: whirlpoolPda.publicKey,
tokenAuthority: context.wallet.publicKey,
tokenAuthority: ctx.wallet.publicKey,
tokenOwnerAccountA: tokenAccountA,
tokenVaultA: tokenVaultAKeypair.publicKey,
tokenOwnerAccountB: tokenAccountB,
@ -62,19 +68,20 @@ describe("update_fees_and_rewards", () => {
tickArray2: tickArrayPda.publicKey,
oracle: oraclePda.publicKey,
})
.buildAndExecute();
).buildAndExecute();
await sleep(1_000);
await client
.updateFeesAndRewards({
await toTx(
ctx,
WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
position: positions[0].publicKey,
tickArrayLower: tickArrayPda.publicKey,
tickArrayUpper: tickArrayPda.publicKey,
})
.buildAndExecute();
const positionAfter = await client.getPosition(positions[0].publicKey);
).buildAndExecute();
const positionAfter = (await fetcher.getPosition(positions[0].publicKey, true)) as PositionData;
assert.ok(positionAfter.feeOwedA.gt(positionBefore.feeOwedA));
assert.ok(positionAfter.feeOwedB.eq(ZERO_BN));
assert.ok(positionAfter.feeGrowthCheckpointA.gt(positionBefore.feeGrowthCheckpointA));
@ -94,7 +101,7 @@ describe("update_fees_and_rewards", () => {
const tickUpperIndex = 33536;
const tickSpacing = TickSpacing.Standard;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing,
positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: ZERO_BN }],
});
@ -103,17 +110,18 @@ describe("update_fees_and_rewards", () => {
positions,
} = fixture.getInfos();
const tickArrayPda = getTickArrayPda(context.program.programId, whirlpoolPda.publicKey, 22528);
const tickArrayPda = PDAUtil.getTickArray(ctx.program.programId, whirlpoolPda.publicKey, 22528);
await assert.rejects(
client
.updateFeesAndRewards({
toTx(
ctx,
WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
position: positions[0].publicKey,
tickArrayLower: tickArrayPda.publicKey,
tickArrayUpper: tickArrayPda.publicKey,
})
.buildAndExecute(),
).buildAndExecute(),
/0x177c/ // LiquidityZero
);
});
@ -125,24 +133,25 @@ describe("update_fees_and_rewards", () => {
const tickSpacing = TickSpacing.Standard;
const {
poolInitInfo: { whirlpoolPda },
} = await initTestPool(client, tickSpacing);
const tickArrayPda = getTickArrayPda(context.program.programId, whirlpoolPda.publicKey, 22528);
} = await initTestPool(ctx, tickSpacing);
const tickArrayPda = PDAUtil.getTickArray(ctx.program.programId, whirlpoolPda.publicKey, 22528);
const other = await new WhirlpoolTestFixture(client).init({
const other = await new WhirlpoolTestFixture(ctx).init({
tickSpacing,
positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: new u64(1_000_000) }],
});
const { positions: otherPositions } = other.getInfos();
await assert.rejects(
client
.updateFeesAndRewards({
toTx(
ctx,
WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
position: otherPositions[0].publicKey,
tickArrayLower: tickArrayPda.publicKey,
tickArrayUpper: tickArrayPda.publicKey,
})
.buildAndExecute(),
).buildAndExecute(),
/0xbbf/ // AccountOwnedByWrongProgram
);
});
@ -153,7 +162,7 @@ describe("update_fees_and_rewards", () => {
const tickUpperIndex = 33536;
const tickSpacing = TickSpacing.Standard;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing,
positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: new anchor.BN(1_000_000) }],
});
@ -162,17 +171,18 @@ describe("update_fees_and_rewards", () => {
positions,
} = fixture.getInfos();
const tickArrayPda = getTickArrayPda(context.program.programId, whirlpoolPda.publicKey, 0);
const tickArrayPda = PDAUtil.getTickArray(ctx.program.programId, whirlpoolPda.publicKey, 0);
await assert.rejects(
client
.updateFeesAndRewards({
toTx(
ctx,
WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
position: positions[0].publicKey,
tickArrayLower: tickArrayPda.publicKey,
tickArrayUpper: tickArrayPda.publicKey,
})
.buildAndExecute(),
).buildAndExecute(),
/0xbbf/ // AccountOwnedByWrongProgram
);
});
@ -183,7 +193,7 @@ describe("update_fees_and_rewards", () => {
const tickUpperIndex = 33536;
const tickSpacing = TickSpacing.Standard;
const fixture = await new WhirlpoolTestFixture(client).init({
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing,
positions: [{ tickLowerIndex, tickUpperIndex, liquidityAmount: ZERO_BN }],
});
@ -194,23 +204,24 @@ describe("update_fees_and_rewards", () => {
const {
poolInitInfo: { whirlpoolPda: otherWhirlpoolPda },
} = await initTestPool(client, tickSpacing);
} = await initTestPool(ctx, tickSpacing);
const tickArrayPda = getTickArrayPda(
context.program.programId,
const tickArrayPda = PDAUtil.getTickArray(
ctx.program.programId,
otherWhirlpoolPda.publicKey,
22528
);
await assert.rejects(
client
.updateFeesAndRewards({
toTx(
ctx,
WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, {
whirlpool: whirlpoolPda.publicKey,
position: positions[0].publicKey,
tickArrayLower: tickArrayPda.publicKey,
tickArrayUpper: tickArrayPda.publicKey,
})
.buildAndExecute(),
).buildAndExecute(),
/0xbbf/ // AccountOwnedByWrongProgram
);
});

Some files were not shown because too many files have changed in this diff Show More