bundled positions (#89)

## Anchor v0.26.0 upgrade (rebased)
Special thanks @dovahcrow !

* anchor 0.26 solana 1.14.12

use mpl-token-metadata 1.7 because rust 1.60 does not compile 1.8

* initialize_pool: ignore passed bump and use one anchor derived

- initialize_pool.test: update error codes, add a test case for ignoring bump

* update expected error messages

* add CHECK comments on UncheckedAccounts

* use create_metadata_accounts_v3 (v2 have been deprecated)

* update unit test cases (cargo test)

## Bundled Positions
* import bundled positions

* upgrade to Anchor v0.26.0 (position bundles related codes)

- ProgramResult to Result<()>
- add /// CHECK comments
- remove space attribute on Mint account
- change create_metadata_accounts_v2 to v3
- update testcases
  - Change in error code detected first
  - Change in account closing method
    https://github.com/coral-xyz/anchor/pull/2169

* cargo fmt

* update seed of BundledPosition

* change temporary mint_authority to position_bundle

* bump to v0.2.0

doc fields are added on IDL

* fix: accidental failure test cases

Fixed test cases that did not take into account rewards accruing up to just before the close.
This test case is not related to bundled positions.
The test case happened to fail, so I fixed them to make them all successful.

---------

Co-authored-by: Weiyuan Wu <weiyuan@crows.land>
This commit is contained in:
yugure-orca 2023-04-07 21:18:56 +09:00 committed by GitHub
parent d18229eb20
commit 44021b15a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
87 changed files with 9044 additions and 577 deletions

1318
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "whirlpool"
version = "0.1.0"
version = "0.2.0"
description = "Created with Anchor"
edition = "2018"
@ -15,14 +15,14 @@ cpi = ["no-entrypoint"]
default = []
[dependencies]
anchor-lang = { git = "https://github.com/project-serum/anchor", tag = "v0.20.1", version = "0.20.1", package = "anchor-lang" }
anchor-spl = { git = "https://github.com/project-serum/anchor", tag = "v0.20.1", version = "0.20.1", package = "anchor-spl" }
spl-token = { version = "3.1.1", features = ["no-entrypoint"] }
solana-program = "1.8.12"
anchor-lang = "0.26"
anchor-spl = "0.26"
spl-token = {version = "3.3", features = ["no-entrypoint"]}
solana-program = "1.14.12"
thiserror = "1.0"
uint = { version = "0.9.1", default-features = false }
uint = {version = "0.9.1", default-features = false}
borsh = "0.9.1"
mpl-token-metadata = { version = "1.2.5", features = ["no-entrypoint"] }
mpl-token-metadata = { version = "1.7", features = ["no-entrypoint"] }
[dev-dependencies]
proptest = "1.0"

View File

@ -1,3 +1,5 @@
pub mod nft;
pub mod test_constants;
pub use nft::*;
pub use test_constants::*;

View File

@ -0,0 +1,18 @@
use anchor_lang::prelude::*;
pub mod whirlpool_nft_update_auth {
use super::*;
declare_id!("3axbTs2z5GBy6usVbNVoqEgZMng3vZvMnAoX29BFfwhr");
}
// METADATA_NAME : max 32 bytes
// METADATA_SYMBOL : max 10 bytes
// METADATA_URI : max 200 bytes
pub const WP_METADATA_NAME: &str = "Orca Whirlpool Position";
pub const WP_METADATA_SYMBOL: &str = "OWP";
pub const WP_METADATA_URI: &str = "https://arweave.net/E19ZNY2sqMqddm1Wx7mrXPUZ0ZZ5ISizhebb0UsVEws";
pub const WPB_METADATA_NAME_PREFIX: &str = "Orca Position Bundle";
pub const WPB_METADATA_SYMBOL: &str = "OPB";
pub const WPB_METADATA_URI: &str =
"https://arweave.net/A_Wo8dx2_3lSUwMIi7bdT_sqxi8soghRNAWXXiqXpgE";

View File

@ -1,8 +1,8 @@
use std::num::TryFromIntError;
use anchor_lang::error;
use anchor_lang::prelude::*;
#[error]
#[error_code]
#[derive(PartialEq)]
pub enum ErrorCode {
#[msg("Enum value could not be converted")]
@ -106,6 +106,15 @@ pub enum ErrorCode {
InvalidIntermediaryMint, //0x1799
#[msg("Duplicate two hop pool")]
DuplicateTwoHopPool, //0x179a
#[msg("Bundle index is out of bounds")]
InvalidBundleIndex, //0x179b
#[msg("Position has already been opened")]
BundledPositionAlreadyOpened, //0x179c
#[msg("Position has already been closed")]
BundledPositionAlreadyClosed, //0x179d
#[msg("Unable to delete PositionBundle with open positions")]
PositionBundleNotDeletable, //0x179e
}
impl From<TryFromIntError> for ErrorCode {

View File

@ -0,0 +1,56 @@
use anchor_lang::prelude::*;
use anchor_spl::token::TokenAccount;
use crate::errors::ErrorCode;
use crate::{state::*, util::verify_position_bundle_authority};
#[derive(Accounts)]
#[instruction(bundle_index: u16)]
pub struct CloseBundledPosition<'info> {
#[account(mut,
close = receiver,
seeds = [
b"bundled_position".as_ref(),
position_bundle.position_bundle_mint.key().as_ref(),
bundle_index.to_string().as_bytes()
],
bump,
)]
pub bundled_position: Account<'info, Position>,
#[account(mut)]
pub position_bundle: Box<Account<'info, PositionBundle>>,
#[account(
constraint = position_bundle_token_account.mint == bundled_position.position_mint,
constraint = position_bundle_token_account.mint == position_bundle.position_bundle_mint,
constraint = position_bundle_token_account.amount == 1
)]
pub position_bundle_token_account: Box<Account<'info, TokenAccount>>,
pub position_bundle_authority: Signer<'info>,
/// CHECK: safe, for receiving rent only
#[account(mut)]
pub receiver: UncheckedAccount<'info>,
}
pub fn handler(ctx: Context<CloseBundledPosition>, bundle_index: u16) -> Result<()> {
let position_bundle = &mut ctx.accounts.position_bundle;
// Allow delegation
verify_position_bundle_authority(
&ctx.accounts.position_bundle_token_account,
&ctx.accounts.position_bundle_authority,
)?;
if !Position::is_position_empty(&ctx.accounts.bundled_position) {
return Err(ErrorCode::ClosePositionNotEmpty.into());
}
position_bundle.close_bundled_position(bundle_index)?;
// Anchor will close the Position account
Ok(())
}

View File

@ -9,10 +9,15 @@ use crate::util::{burn_and_close_user_position_token, verify_position_authority}
pub struct ClosePosition<'info> {
pub position_authority: Signer<'info>,
/// CHECK: safe, for receiving rent only
#[account(mut)]
pub receiver: UncheckedAccount<'info>,
#[account(mut, close = receiver)]
#[account(mut,
close = receiver,
seeds = [b"position".as_ref(), position_mint.key().as_ref()],
bump,
)]
pub position: Account<'info, Position>,
#[account(mut, address = position.position_mint)]
@ -27,7 +32,7 @@ pub struct ClosePosition<'info> {
pub token_program: Program<'info, Token>,
}
pub fn handler(ctx: Context<ClosePosition>) -> ProgramResult {
pub fn handler(ctx: Context<ClosePosition>) -> Result<()> {
verify_position_authority(
&ctx.accounts.position_token_account,
&ctx.accounts.position_authority,

View File

@ -34,7 +34,7 @@ pub struct CollectFees<'info> {
pub token_program: Program<'info, Token>,
}
pub fn handler(ctx: Context<CollectFees>) -> ProgramResult {
pub fn handler(ctx: Context<CollectFees>) -> Result<()> {
verify_position_authority(
&ctx.accounts.position_token_account,
&ctx.accounts.position_authority,

View File

@ -28,7 +28,7 @@ pub struct CollectProtocolFees<'info> {
pub token_program: Program<'info, Token>,
}
pub fn handler(ctx: Context<CollectProtocolFees>) -> ProgramResult {
pub fn handler(ctx: Context<CollectProtocolFees>) -> Result<()> {
let whirlpool = &ctx.accounts.whirlpool;
transfer_from_vault_to_owner(

View File

@ -46,7 +46,7 @@ pub struct CollectReward<'info> {
/// - `Ok`: Reward tokens at the specified reward index have been successfully harvested
/// - `Err`: `RewardNotInitialized` if the specified reward has not been initialized
/// `InvalidRewardIndex` if the reward index is not 0, 1, or 2
pub fn handler(ctx: Context<CollectReward>, reward_index: u8) -> ProgramResult {
pub fn handler(ctx: Context<CollectReward>, reward_index: u8) -> Result<()> {
verify_position_authority(
&ctx.accounts.position_token_account,
&ctx.accounts.position_authority,

View File

@ -17,7 +17,7 @@ pub fn handler(
liquidity_amount: u128,
token_min_a: u64,
token_min_b: u64,
) -> ProgramResult {
) -> Result<()> {
verify_position_authority(
&ctx.accounts.position_token_account,
&ctx.accounts.position_authority,

View File

@ -0,0 +1,47 @@
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Mint, Token, TokenAccount};
use crate::errors::ErrorCode;
use crate::state::*;
use crate::util::burn_and_close_position_bundle_token;
#[derive(Accounts)]
pub struct DeletePositionBundle<'info> {
#[account(mut, close = receiver)]
pub position_bundle: Account<'info, PositionBundle>,
#[account(mut, address = position_bundle.position_bundle_mint)]
pub position_bundle_mint: Account<'info, Mint>,
#[account(mut,
constraint = position_bundle_token_account.mint == position_bundle.position_bundle_mint,
constraint = position_bundle_token_account.owner == position_bundle_owner.key(),
constraint = position_bundle_token_account.amount == 1,
)]
pub position_bundle_token_account: Box<Account<'info, TokenAccount>>,
pub position_bundle_owner: Signer<'info>,
/// CHECK: safe, for receiving rent only
#[account(mut)]
pub receiver: UncheckedAccount<'info>,
#[account(address = token::ID)]
pub token_program: Program<'info, Token>,
}
pub fn handler(ctx: Context<DeletePositionBundle>) -> Result<()> {
let position_bundle = &ctx.accounts.position_bundle;
if !position_bundle.is_deletable() {
return Err(ErrorCode::PositionBundleNotDeletable.into());
}
burn_and_close_position_bundle_token(
&ctx.accounts.position_bundle_owner,
&ctx.accounts.receiver,
&ctx.accounts.position_bundle_mint,
&ctx.accounts.position_bundle_token_account,
&ctx.accounts.token_program,
)
}

View File

@ -48,7 +48,7 @@ pub fn handler(
liquidity_amount: u128,
token_max_a: u64,
token_max_b: u64,
) -> ProgramResult {
) -> Result<()> {
verify_position_authority(
&ctx.accounts.position_token_account,
&ctx.accounts.position_authority,

View File

@ -19,7 +19,7 @@ pub fn handler(
collect_protocol_fees_authority: Pubkey,
reward_emissions_super_authority: Pubkey,
default_protocol_fee_rate: u16,
) -> ProgramResult {
) -> Result<()> {
let config = &mut ctx.accounts.config;
Ok(config.initialize(

View File

@ -27,7 +27,7 @@ pub fn handler(
ctx: Context<InitializeFeeTier>,
tick_spacing: u16,
default_fee_rate: u16,
) -> ProgramResult {
) -> Result<()> {
Ok(ctx
.accounts
.fee_tier

View File

@ -21,7 +21,7 @@ pub struct InitializePool<'info> {
token_mint_b.key().as_ref(),
tick_spacing.to_le_bytes().as_ref()
],
bump = bumps.whirlpool_bump,
bump,
payer = funder,
space = Whirlpool::LEN)]
pub whirlpool: Box<Account<'info, Whirlpool>>,
@ -49,10 +49,10 @@ pub struct InitializePool<'info> {
pub fn handler(
ctx: Context<InitializePool>,
bumps: WhirlpoolBumps,
_bumps: WhirlpoolBumps,
tick_spacing: u16,
initial_sqrt_price: u128,
) -> ProgramResult {
) -> Result<()> {
let token_mint_a = ctx.accounts.token_mint_a.key();
let token_mint_b = ctx.accounts.token_mint_b.key();
@ -61,9 +61,12 @@ pub fn handler(
let default_fee_rate = ctx.accounts.fee_tier.default_fee_rate;
// ignore the bump passed and use one Anchor derived
let bump = *ctx.bumps.get("whirlpool").unwrap();
Ok(whirlpool.initialize(
whirlpools_config,
bumps.whirlpool_bump,
bump,
tick_spacing,
initial_sqrt_price,
default_fee_rate,

View File

@ -0,0 +1,63 @@
use anchor_lang::prelude::*;
use anchor_spl::associated_token::AssociatedToken;
use anchor_spl::token::{self, Mint, Token, TokenAccount};
use crate::{state::*, util::mint_position_bundle_token_and_remove_authority};
#[derive(Accounts)]
pub struct InitializePositionBundle<'info> {
#[account(init,
payer = funder,
space = PositionBundle::LEN,
seeds = [b"position_bundle".as_ref(), position_bundle_mint.key().as_ref()],
bump,
)]
pub position_bundle: Box<Account<'info, PositionBundle>>,
#[account(init,
payer = funder,
mint::authority = position_bundle, // will be removed in the transaction
mint::decimals = 0,
)]
pub position_bundle_mint: Account<'info, Mint>,
#[account(init,
payer = funder,
associated_token::mint = position_bundle_mint,
associated_token::authority = position_bundle_owner,
)]
pub position_bundle_token_account: Box<Account<'info, TokenAccount>>,
/// CHECK: safe, the account that will be the owner of the position bundle can be arbitrary
pub position_bundle_owner: UncheckedAccount<'info>,
#[account(mut)]
pub funder: Signer<'info>,
#[account(address = token::ID)]
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,
pub associated_token_program: Program<'info, AssociatedToken>,
}
pub fn handler(ctx: Context<InitializePositionBundle>) -> Result<()> {
let position_bundle_mint = &ctx.accounts.position_bundle_mint;
let position_bundle = &mut ctx.accounts.position_bundle;
position_bundle.initialize(position_bundle_mint.key())?;
let bump = *ctx.bumps.get("position_bundle").unwrap();
mint_position_bundle_token_and_remove_authority(
&ctx.accounts.position_bundle,
position_bundle_mint,
&ctx.accounts.position_bundle_token_account,
&ctx.accounts.token_program,
&[
b"position_bundle".as_ref(),
position_bundle_mint.key().as_ref(),
&[bump],
],
)
}

View File

@ -0,0 +1,83 @@
use anchor_lang::prelude::*;
use anchor_spl::associated_token::AssociatedToken;
use anchor_spl::token::{self, Mint, Token, TokenAccount};
use crate::constants::nft::whirlpool_nft_update_auth::ID as WPB_NFT_UPDATE_AUTH;
use crate::{state::*, util::mint_position_bundle_token_with_metadata_and_remove_authority};
#[derive(Accounts)]
pub struct InitializePositionBundleWithMetadata<'info> {
#[account(init,
payer = funder,
space = PositionBundle::LEN,
seeds = [b"position_bundle".as_ref(), position_bundle_mint.key().as_ref()],
bump,
)]
pub position_bundle: Box<Account<'info, PositionBundle>>,
#[account(init,
payer = funder,
mint::authority = position_bundle, // will be removed in the transaction
mint::decimals = 0,
)]
pub position_bundle_mint: Account<'info, Mint>,
/// CHECK: checked via the Metadata CPI call
/// https://github.com/metaplex-foundation/metaplex-program-library/blob/773a574c4b34e5b9f248a81306ec24db064e255f/token-metadata/program/src/utils/metadata.rs#L100
#[account(mut)]
pub position_bundle_metadata: UncheckedAccount<'info>,
#[account(init,
payer = funder,
associated_token::mint = position_bundle_mint,
associated_token::authority = position_bundle_owner,
)]
pub position_bundle_token_account: Box<Account<'info, TokenAccount>>,
/// CHECK: safe, the account that will be the owner of the position bundle can be arbitrary
pub position_bundle_owner: UncheckedAccount<'info>,
#[account(mut)]
pub funder: Signer<'info>,
/// CHECK: checked via account constraints
#[account(address = WPB_NFT_UPDATE_AUTH)]
pub metadata_update_auth: UncheckedAccount<'info>,
#[account(address = token::ID)]
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,
pub associated_token_program: Program<'info, AssociatedToken>,
/// CHECK: checked via account constraints
#[account(address = mpl_token_metadata::ID)]
pub metadata_program: UncheckedAccount<'info>,
}
pub fn handler(ctx: Context<InitializePositionBundleWithMetadata>) -> Result<()> {
let position_bundle_mint = &ctx.accounts.position_bundle_mint;
let position_bundle = &mut ctx.accounts.position_bundle;
position_bundle.initialize(position_bundle_mint.key())?;
let bump = *ctx.bumps.get("position_bundle").unwrap();
mint_position_bundle_token_with_metadata_and_remove_authority(
&ctx.accounts.funder,
&ctx.accounts.position_bundle,
position_bundle_mint,
&ctx.accounts.position_bundle_token_account,
&ctx.accounts.position_bundle_metadata,
&ctx.accounts.metadata_update_auth,
&ctx.accounts.metadata_program,
&ctx.accounts.token_program,
&ctx.accounts.system_program,
&ctx.accounts.rent,
&[
b"position_bundle".as_ref(),
position_bundle_mint.key().as_ref(),
&[bump],
],
)
}

View File

@ -31,7 +31,7 @@ pub struct InitializeReward<'info> {
pub rent: Sysvar<'info, Rent>,
}
pub fn handler(ctx: Context<InitializeReward>, reward_index: u8) -> ProgramResult {
pub fn handler(ctx: Context<InitializeReward>, reward_index: u8) -> Result<()> {
let whirlpool = &mut ctx.accounts.whirlpool;
Ok(whirlpool.initialize_reward(

View File

@ -21,7 +21,7 @@ pub struct InitializeTickArray<'info> {
pub system_program: Program<'info, System>,
}
pub fn handler(ctx: Context<InitializeTickArray>, start_tick_index: i32) -> ProgramResult {
pub fn handler(ctx: Context<InitializeTickArray>, start_tick_index: i32) -> Result<()> {
let mut tick_array = ctx.accounts.tick_array.load_init()?;
Ok(tick_array.initialize(&ctx.accounts.whirlpool, start_tick_index)?)
}

View File

@ -1,14 +1,19 @@
pub mod close_bundled_position;
pub mod close_position;
pub mod collect_fees;
pub mod collect_protocol_fees;
pub mod collect_reward;
pub mod decrease_liquidity;
pub mod delete_position_bundle;
pub mod increase_liquidity;
pub mod initialize_config;
pub mod initialize_fee_tier;
pub mod initialize_pool;
pub mod initialize_position_bundle;
pub mod initialize_position_bundle_with_metadata;
pub mod initialize_reward;
pub mod initialize_tick_array;
pub mod open_bundled_position;
pub mod open_position;
pub mod open_position_with_metadata;
pub mod set_collect_protocol_fees_authority;
@ -25,17 +30,22 @@ pub mod swap;
pub mod two_hop_swap;
pub mod update_fees_and_rewards;
pub use close_bundled_position::*;
pub use close_position::*;
pub use collect_fees::*;
pub use collect_protocol_fees::*;
pub use collect_reward::*;
pub use decrease_liquidity::*;
pub use delete_position_bundle::*;
pub use increase_liquidity::*;
pub use initialize_config::*;
pub use initialize_fee_tier::*;
pub use initialize_pool::*;
pub use initialize_position_bundle::*;
pub use initialize_position_bundle_with_metadata::*;
pub use initialize_reward::*;
pub use initialize_tick_array::*;
pub use open_bundled_position::*;
pub use open_position::*;
pub use open_position_with_metadata::*;
pub use set_collect_protocol_fees_authority::*;

View File

@ -0,0 +1,67 @@
use anchor_lang::prelude::*;
use anchor_spl::token::TokenAccount;
use crate::{state::*, util::verify_position_bundle_authority};
#[derive(Accounts)]
#[instruction(bundle_index: u16)]
pub struct OpenBundledPosition<'info> {
#[account(init,
payer = funder,
space = Position::LEN,
seeds = [
b"bundled_position".as_ref(),
position_bundle.position_bundle_mint.key().as_ref(),
bundle_index.to_string().as_bytes()
],
bump,
)]
pub bundled_position: Box<Account<'info, Position>>,
#[account(mut)]
pub position_bundle: Box<Account<'info, PositionBundle>>,
#[account(
constraint = position_bundle_token_account.mint == position_bundle.position_bundle_mint,
constraint = position_bundle_token_account.amount == 1
)]
pub position_bundle_token_account: Box<Account<'info, TokenAccount>>,
pub position_bundle_authority: Signer<'info>,
pub whirlpool: Box<Account<'info, Whirlpool>>,
#[account(mut)]
pub funder: Signer<'info>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,
}
pub fn handler(
ctx: Context<OpenBundledPosition>,
bundle_index: u16,
tick_lower_index: i32,
tick_upper_index: i32,
) -> Result<()> {
let whirlpool = &ctx.accounts.whirlpool;
let position_bundle = &mut ctx.accounts.position_bundle;
let position = &mut ctx.accounts.bundled_position;
// Allow delegation
verify_position_bundle_authority(
&ctx.accounts.position_bundle_token_account,
&ctx.accounts.position_bundle_authority,
)?;
position_bundle.open_bundled_position(bundle_index)?;
position.open_position(
whirlpool,
position_bundle.position_bundle_mint,
tick_lower_index,
tick_upper_index,
)?;
Ok(())
}

View File

@ -10,19 +10,19 @@ pub struct OpenPosition<'info> {
#[account(mut)]
pub funder: Signer<'info>,
/// CHECK: safe, the account that will be the owner of the position can be arbitrary
pub owner: UncheckedAccount<'info>,
#[account(init,
payer = funder,
space = Position::LEN,
seeds = [b"position".as_ref(), position_mint.key().as_ref()],
bump = bumps.position_bump,
bump,
)]
pub position: Box<Account<'info, Position>>,
#[account(init,
payer = funder,
space = Mint::LEN,
mint::authority = whirlpool,
mint::decimals = 0,
)]
@ -52,7 +52,7 @@ pub fn handler(
_bumps: OpenPositionBumps,
tick_lower_index: i32,
tick_upper_index: i32,
) -> ProgramResult {
) -> Result<()> {
let whirlpool = &ctx.accounts.whirlpool;
let position_mint = &ctx.accounts.position_mint;
let position = &mut ctx.accounts.position;

View File

@ -4,11 +4,7 @@ use anchor_spl::token::{self, Mint, Token, TokenAccount};
use crate::{state::*, util::mint_position_token_with_metadata_and_remove_authority};
use whirlpool_nft_update_auth::ID as WP_NFT_UPDATE_AUTH;
mod whirlpool_nft_update_auth {
use super::*;
declare_id!("3axbTs2z5GBy6usVbNVoqEgZMng3vZvMnAoX29BFfwhr");
}
use crate::constants::nft::whirlpool_nft_update_auth::ID as WP_NFT_UPDATE_AUTH;
#[derive(Accounts)]
#[instruction(bumps: OpenPositionWithMetadataBumps)]
@ -16,19 +12,19 @@ pub struct OpenPositionWithMetadata<'info> {
#[account(mut)]
pub funder: Signer<'info>,
/// CHECK: safe, the account that will be the owner of the position can be arbitrary
pub owner: UncheckedAccount<'info>,
#[account(init,
payer = funder,
space = Position::LEN,
seeds = [b"position".as_ref(), position_mint.key().as_ref()],
bump = bumps.position_bump,
bump,
)]
pub position: Box<Account<'info, Position>>,
#[account(init,
payer = funder,
space = Mint::LEN,
mint::authority = whirlpool,
mint::decimals = 0,
)]
@ -71,7 +67,7 @@ pub fn handler(
_bumps: OpenPositionWithMetadataBumps,
tick_lower_index: i32,
tick_upper_index: i32,
) -> ProgramResult {
) -> Result<()> {
let whirlpool = &ctx.accounts.whirlpool;
let position_mint = &ctx.accounts.position_mint;
let position = &mut ctx.accounts.position;

View File

@ -10,10 +10,11 @@ pub struct SetCollectProtocolFeesAuthority<'info> {
#[account(address = whirlpools_config.collect_protocol_fees_authority)]
pub collect_protocol_fees_authority: Signer<'info>,
/// CHECK: safe, the account that will be new authority can be arbitrary
pub new_collect_protocol_fees_authority: UncheckedAccount<'info>,
}
pub fn handler(ctx: Context<SetCollectProtocolFeesAuthority>) -> ProgramResult {
pub fn handler(ctx: Context<SetCollectProtocolFeesAuthority>) -> Result<()> {
Ok(ctx
.accounts
.whirlpools_config

View File

@ -16,7 +16,7 @@ pub struct SetDefaultFeeRate<'info> {
/*
Updates the default fee rate on a FeeTier object.
*/
pub fn handler(ctx: Context<SetDefaultFeeRate>, default_fee_rate: u16) -> ProgramResult {
pub fn handler(ctx: Context<SetDefaultFeeRate>, default_fee_rate: u16) -> Result<()> {
Ok(ctx
.accounts
.fee_tier

View File

@ -14,7 +14,7 @@ pub struct SetDefaultProtocolFeeRate<'info> {
pub fn handler(
ctx: Context<SetDefaultProtocolFeeRate>,
default_protocol_fee_rate: u16,
) -> ProgramResult {
) -> Result<()> {
Ok(ctx
.accounts
.whirlpools_config

View File

@ -10,11 +10,12 @@ pub struct SetFeeAuthority<'info> {
#[account(address = whirlpools_config.fee_authority)]
pub fee_authority: Signer<'info>,
/// CHECK: safe, the account that will be new authority can be arbitrary
pub new_fee_authority: UncheckedAccount<'info>,
}
/// Set the fee authority. Only the current fee authority has permission to invoke this instruction.
pub fn handler(ctx: Context<SetFeeAuthority>) -> ProgramResult {
pub fn handler(ctx: Context<SetFeeAuthority>) -> Result<()> {
Ok(ctx
.accounts
.whirlpools_config

View File

@ -13,6 +13,6 @@ pub struct SetFeeRate<'info> {
pub fee_authority: Signer<'info>,
}
pub fn handler(ctx: Context<SetFeeRate>, fee_rate: u16) -> ProgramResult {
pub fn handler(ctx: Context<SetFeeRate>, fee_rate: u16) -> Result<()> {
Ok(ctx.accounts.whirlpool.update_fee_rate(fee_rate)?)
}

View File

@ -13,7 +13,7 @@ pub struct SetProtocolFeeRate<'info> {
pub fee_authority: Signer<'info>,
}
pub fn handler(ctx: Context<SetProtocolFeeRate>, protocol_fee_rate: u16) -> ProgramResult {
pub fn handler(ctx: Context<SetProtocolFeeRate>, protocol_fee_rate: u16) -> Result<()> {
Ok(ctx
.accounts
.whirlpool

View File

@ -11,10 +11,11 @@ pub struct SetRewardAuthority<'info> {
#[account(address = whirlpool.reward_infos[reward_index as usize].authority)]
pub reward_authority: Signer<'info>,
/// CHECK: safe, the account that will be new authority can be arbitrary
pub new_reward_authority: UncheckedAccount<'info>,
}
pub fn handler(ctx: Context<SetRewardAuthority>, reward_index: u8) -> ProgramResult {
pub fn handler(ctx: Context<SetRewardAuthority>, reward_index: u8) -> Result<()> {
Ok(ctx.accounts.whirlpool.update_reward_authority(
reward_index as usize,
ctx.accounts.new_reward_authority.key(),

View File

@ -13,15 +13,13 @@ pub struct SetRewardAuthorityBySuperAuthority<'info> {
#[account(address = whirlpools_config.reward_emissions_super_authority)]
pub reward_emissions_super_authority: Signer<'info>,
/// CHECK: safe, the account that will be new authority can be arbitrary
pub new_reward_authority: UncheckedAccount<'info>,
}
/// Set the whirlpool reward authority at the provided `reward_index`.
/// Only the current reward emissions super authority has permission to invoke this instruction.
pub fn handler(
ctx: Context<SetRewardAuthorityBySuperAuthority>,
reward_index: u8,
) -> ProgramResult {
pub fn handler(ctx: Context<SetRewardAuthorityBySuperAuthority>, reward_index: u8) -> Result<()> {
Ok(ctx.accounts.whirlpool.update_reward_authority(
reward_index as usize,
ctx.accounts.new_reward_authority.key(),

View File

@ -26,7 +26,7 @@ pub fn handler(
ctx: Context<SetRewardEmissions>,
reward_index: u8,
emissions_per_second_x64: u128,
) -> ProgramResult {
) -> Result<()> {
let whirlpool = &ctx.accounts.whirlpool;
let reward_vault = &ctx.accounts.reward_vault;

View File

@ -10,10 +10,11 @@ pub struct SetRewardEmissionsSuperAuthority<'info> {
#[account(address = whirlpools_config.reward_emissions_super_authority)]
pub reward_emissions_super_authority: Signer<'info>,
/// CHECK: safe, the account that will be new authority can be arbitrary
pub new_reward_emissions_super_authority: UncheckedAccount<'info>,
}
pub fn handler(ctx: Context<SetRewardEmissionsSuperAuthority>) -> ProgramResult {
pub fn handler(ctx: Context<SetRewardEmissionsSuperAuthority>) -> Result<()> {
Ok(ctx
.accounts
.whirlpools_config

View File

@ -5,11 +5,7 @@ use crate::{
errors::ErrorCode,
manager::swap_manager::*,
state::{TickArray, Whirlpool},
util::{
to_timestamp_u64,
SwapTickSequence,
update_and_swap_whirlpool
},
util::{to_timestamp_u64, update_and_swap_whirlpool, SwapTickSequence},
};
#[derive(Accounts)]
@ -42,7 +38,7 @@ pub struct Swap<'info> {
pub tick_array_2: AccountLoader<'info, TickArray>,
#[account(seeds = [b"oracle", whirlpool.key().as_ref()],bump)]
/// Oracle is currently unused and will be enabled on subsequent updates
/// CHECK: Oracle is currently unused and will be enabled on subsequent updates
pub oracle: UncheckedAccount<'info>,
}
@ -53,7 +49,7 @@ pub fn handler(
sqrt_price_limit: u128,
amount_specified_is_input: bool,
a_to_b: bool, // Zero for one
) -> ProgramResult {
) -> Result<()> {
let whirlpool = &mut ctx.accounts.whirlpool;
let clock = Clock::get()?;
// Update the global reward growth which increases as a function of time.

View File

@ -5,11 +5,7 @@ use crate::{
errors::ErrorCode,
manager::swap_manager::*,
state::{TickArray, Whirlpool},
util::{
to_timestamp_u64,
SwapTickSequence,
update_and_swap_whirlpool,
},
util::{to_timestamp_u64, update_and_swap_whirlpool, SwapTickSequence},
};
#[derive(Accounts)]
@ -64,11 +60,11 @@ pub struct TwoHopSwap<'info> {
pub tick_array_two_2: AccountLoader<'info, TickArray>,
#[account(seeds = [b"oracle", whirlpool_one.key().as_ref()],bump)]
/// Oracle is currently unused and will be enabled on subsequent updates
/// CHECK: Oracle is currently unused and will be enabled on subsequent updates
pub oracle_one: UncheckedAccount<'info>,
#[account(seeds = [b"oracle", whirlpool_two.key().as_ref()],bump)]
/// Oracle is currently unused and will be enabled on subsequent updates
/// CHECK: Oracle is currently unused and will be enabled on subsequent updates
pub oracle_two: UncheckedAccount<'info>,
}
@ -81,7 +77,7 @@ pub fn handler(
a_to_b_two: bool,
sqrt_price_limit_one: u128,
sqrt_price_limit_two: u128,
) -> ProgramResult {
) -> Result<()> {
let clock = Clock::get()?;
// Update the global reward growth which increases as a function of time.
let timestamp = to_timestamp_u64(clock.unix_timestamp)?;
@ -115,7 +111,7 @@ pub fn handler(
ctx.accounts.tick_array_one_2.load_mut().ok(),
);
let mut swap_tick_sequence_two= SwapTickSequence::new(
let mut swap_tick_sequence_two = SwapTickSequence::new(
ctx.accounts.tick_array_two_0.load_mut().unwrap(),
ctx.accounts.tick_array_two_1.load_mut().ok(),
ctx.accounts.tick_array_two_2.load_mut().ok(),

View File

@ -1,5 +1,3 @@
use anchor_lang::prelude::ProgramResult;
use anchor_lang::prelude::*;
use crate::{
@ -20,7 +18,7 @@ pub struct UpdateFeesAndRewards<'info> {
pub tick_array_upper: AccountLoader<'info, TickArray>,
}
pub fn handler(ctx: Context<UpdateFeesAndRewards>) -> ProgramResult {
pub fn handler(ctx: Context<UpdateFeesAndRewards>) -> Result<()> {
let whirlpool = &mut ctx.accounts.whirlpool;
let position = &mut ctx.accounts.position;
let clock = Clock::get()?;

View File

@ -39,7 +39,7 @@ pub mod whirlpool {
collect_protocol_fees_authority: Pubkey,
reward_emissions_super_authority: Pubkey,
default_protocol_fee_rate: u16,
) -> ProgramResult {
) -> Result<()> {
return instructions::initialize_config::handler(
ctx,
fee_authority,
@ -66,7 +66,7 @@ pub mod whirlpool {
bumps: WhirlpoolBumps,
tick_spacing: u16,
initial_sqrt_price: u128,
) -> ProgramResult {
) -> Result<()> {
return instructions::initialize_pool::handler(
ctx,
bumps,
@ -87,7 +87,7 @@ pub mod whirlpool {
pub fn initialize_tick_array(
ctx: Context<InitializeTickArray>,
start_tick_index: i32,
) -> ProgramResult {
) -> Result<()> {
return instructions::initialize_tick_array::handler(ctx, start_tick_index);
}
@ -107,7 +107,7 @@ pub mod whirlpool {
ctx: Context<InitializeFeeTier>,
tick_spacing: u16,
default_fee_rate: u16,
) -> ProgramResult {
) -> Result<()> {
return instructions::initialize_fee_tier::handler(ctx, tick_spacing, default_fee_rate);
}
@ -124,7 +124,7 @@ pub mod whirlpool {
/// - `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 initialize_reward(ctx: Context<InitializeReward>, reward_index: u8) -> ProgramResult {
pub fn initialize_reward(ctx: Context<InitializeReward>, reward_index: u8) -> Result<()> {
return instructions::initialize_reward::handler(ctx, reward_index);
}
@ -149,7 +149,7 @@ pub mod whirlpool {
ctx: Context<SetRewardEmissions>,
reward_index: u8,
emissions_per_second_x64: u128,
) -> ProgramResult {
) -> Result<()> {
return instructions::set_reward_emissions::handler(
ctx,
reward_index,
@ -172,7 +172,7 @@ pub mod whirlpool {
bumps: OpenPositionBumps,
tick_lower_index: i32,
tick_upper_index: i32,
) -> ProgramResult {
) -> Result<()> {
return instructions::open_position::handler(
ctx,
bumps,
@ -197,7 +197,7 @@ pub mod whirlpool {
bumps: OpenPositionWithMetadataBumps,
tick_lower_index: i32,
tick_upper_index: i32,
) -> ProgramResult {
) -> Result<()> {
return instructions::open_position_with_metadata::handler(
ctx,
bumps,
@ -225,7 +225,7 @@ pub mod whirlpool {
liquidity_amount: u128,
token_max_a: u64,
token_max_b: u64,
) -> ProgramResult {
) -> Result<()> {
return instructions::increase_liquidity::handler(
ctx,
liquidity_amount,
@ -253,7 +253,7 @@ pub mod whirlpool {
liquidity_amount: u128,
token_min_a: u64,
token_min_b: u64,
) -> ProgramResult {
) -> Result<()> {
return instructions::decrease_liquidity::handler(
ctx,
liquidity_amount,
@ -267,7 +267,7 @@ pub mod whirlpool {
/// #### 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 {
pub fn update_fees_and_rewards(ctx: Context<UpdateFeesAndRewards>) -> Result<()> {
return instructions::update_fees_and_rewards::handler(ctx);
}
@ -275,7 +275,7 @@ pub mod whirlpool {
///
/// ### Authority
/// - `position_authority` - authority that owns the token corresponding to this desired position.
pub fn collect_fees(ctx: Context<CollectFees>) -> ProgramResult {
pub fn collect_fees(ctx: Context<CollectFees>) -> Result<()> {
return instructions::collect_fees::handler(ctx);
}
@ -283,7 +283,7 @@ pub mod whirlpool {
///
/// ### Authority
/// - `position_authority` - authority that owns the token corresponding to this desired position.
pub fn collect_reward(ctx: Context<CollectReward>, reward_index: u8) -> ProgramResult {
pub fn collect_reward(ctx: Context<CollectReward>, reward_index: u8) -> Result<()> {
return instructions::collect_reward::handler(ctx, reward_index);
}
@ -291,7 +291,7 @@ pub mod whirlpool {
///
/// ### Authority
/// - `collect_protocol_fees_authority` - assigned authority in the WhirlpoolConfig that can collect protocol fees
pub fn collect_protocol_fees(ctx: Context<CollectProtocolFees>) -> ProgramResult {
pub fn collect_protocol_fees(ctx: Context<CollectProtocolFees>) -> Result<()> {
return instructions::collect_protocol_fees::handler(ctx);
}
@ -323,7 +323,7 @@ pub mod whirlpool {
sqrt_price_limit: u128,
amount_specified_is_input: bool,
a_to_b: bool,
) -> ProgramResult {
) -> Result<()> {
return instructions::swap::handler(
ctx,
amount,
@ -341,7 +341,7 @@ pub mod whirlpool {
///
/// #### Special Errors
/// - `ClosePositionNotEmpty` - The provided position account is not empty.
pub fn close_position(ctx: Context<ClosePosition>) -> ProgramResult {
pub fn close_position(ctx: Context<ClosePosition>) -> Result<()> {
return instructions::close_position::handler(ctx);
}
@ -360,7 +360,7 @@ pub mod whirlpool {
pub fn set_default_fee_rate(
ctx: Context<SetDefaultFeeRate>,
default_fee_rate: u16,
) -> ProgramResult {
) -> Result<()> {
return instructions::set_default_fee_rate::handler(ctx, default_fee_rate);
}
@ -379,7 +379,7 @@ pub mod whirlpool {
pub fn set_default_protocol_fee_rate(
ctx: Context<SetDefaultProtocolFeeRate>,
default_protocol_fee_rate: u16,
) -> ProgramResult {
) -> Result<()> {
return instructions::set_default_protocol_fee_rate::handler(
ctx,
default_protocol_fee_rate,
@ -398,7 +398,7 @@ pub mod whirlpool {
///
/// #### Special Errors
/// - `FeeRateMaxExceeded` - If the provided fee_rate exceeds MAX_FEE_RATE.
pub fn set_fee_rate(ctx: Context<SetFeeRate>, fee_rate: u16) -> ProgramResult {
pub fn set_fee_rate(ctx: Context<SetFeeRate>, fee_rate: u16) -> Result<()> {
return instructions::set_fee_rate::handler(ctx, fee_rate);
}
@ -417,7 +417,7 @@ pub mod whirlpool {
pub fn set_protocol_fee_rate(
ctx: Context<SetProtocolFeeRate>,
protocol_fee_rate: u16,
) -> ProgramResult {
) -> Result<()> {
return instructions::set_protocol_fee_rate::handler(ctx, protocol_fee_rate);
}
@ -428,7 +428,7 @@ pub mod whirlpool {
///
/// ### Authority
/// - "fee_authority" - Set authority that can modify pool fees in the WhirlpoolConfig
pub fn set_fee_authority(ctx: Context<SetFeeAuthority>) -> ProgramResult {
pub fn set_fee_authority(ctx: Context<SetFeeAuthority>) -> Result<()> {
return instructions::set_fee_authority::handler(ctx);
}
@ -439,7 +439,7 @@ pub mod whirlpool {
/// - "fee_authority" - Set authority that can collect protocol fees in the WhirlpoolConfig
pub fn set_collect_protocol_fees_authority(
ctx: Context<SetCollectProtocolFeesAuthority>,
) -> ProgramResult {
) -> Result<()> {
return instructions::set_collect_protocol_fees_authority::handler(ctx);
}
@ -453,10 +453,7 @@ pub mod whirlpool {
/// - `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,
) -> ProgramResult {
pub fn set_reward_authority(ctx: Context<SetRewardAuthority>, reward_index: u8) -> Result<()> {
return instructions::set_reward_authority::handler(ctx, reward_index);
}
@ -473,7 +470,7 @@ pub mod whirlpool {
pub fn set_reward_authority_by_super_authority(
ctx: Context<SetRewardAuthorityBySuperAuthority>,
reward_index: u8,
) -> ProgramResult {
) -> Result<()> {
return instructions::set_reward_authority_by_super_authority::handler(ctx, reward_index);
}
@ -485,7 +482,7 @@ pub mod whirlpool {
/// - "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>,
) -> ProgramResult {
) -> Result<()> {
return instructions::set_reward_emissions_super_authority::handler(ctx);
}
@ -523,7 +520,7 @@ pub mod whirlpool {
a_to_b_two: bool,
sqrt_price_limit_one: u128,
sqrt_price_limit_two: u128,
) -> ProgramResult {
) -> Result<()> {
return instructions::two_hop_swap::handler(
ctx,
amount,
@ -535,4 +532,78 @@ pub mod whirlpool {
sqrt_price_limit_two,
);
}
/// Initializes a PositionBundle account that bundles several positions.
/// A unique token will be minted to represent the position bundle in the users wallet.
pub fn initialize_position_bundle(ctx: Context<InitializePositionBundle>) -> Result<()> {
return instructions::initialize_position_bundle::handler(ctx);
}
/// Initializes a PositionBundle account that bundles several positions.
/// A unique token will be minted to represent the position bundle in the users wallet.
/// Additional Metaplex metadata is appended to identify the token.
pub fn initialize_position_bundle_with_metadata(
ctx: Context<InitializePositionBundleWithMetadata>,
) -> Result<()> {
return instructions::initialize_position_bundle_with_metadata::handler(ctx);
}
/// Delete a PositionBundle account. Burns the position bundle token in the owner's wallet.
///
/// ### Authority
/// - `position_bundle_owner` - The owner that owns the position bundle token.
///
/// ### Special Errors
/// - `PositionBundleNotDeletable` - The provided position bundle has open positions.
pub fn delete_position_bundle(ctx: Context<DeletePositionBundle>) -> Result<()> {
return instructions::delete_position_bundle::handler(ctx);
}
/// Open a bundled position in a Whirlpool. No new tokens are issued
/// because the owner of the position bundle becomes the owner of the position.
/// The position will start off with 0 liquidity.
///
/// ### Authority
/// - `position_bundle_authority` - authority that owns the token corresponding to this desired position bundle.
///
/// ### Parameters
/// - `bundle_index` - The bundle index that we'd like to open.
/// - `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
/// - `InvalidBundleIndex` - If the provided bundle index is out of bounds.
/// - `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_bundled_position(
ctx: Context<OpenBundledPosition>,
bundle_index: u16,
tick_lower_index: i32,
tick_upper_index: i32,
) -> Result<()> {
return instructions::open_bundled_position::handler(
ctx,
bundle_index,
tick_lower_index,
tick_upper_index,
);
}
/// Close a bundled position in a Whirlpool.
///
/// ### Authority
/// - `position_bundle_authority` - authority that owns the token corresponding to this desired position bundle.
///
/// ### Parameters
/// - `bundle_index` - The bundle index that we'd like to close.
///
/// #### Special Errors
/// - `InvalidBundleIndex` - If the provided bundle index is out of bounds.
/// - `ClosePositionNotEmpty` - The provided position account is not empty.
pub fn close_bundled_position(
ctx: Context<CloseBundledPosition>,
bundle_index: u16,
) -> Result<()> {
return instructions::close_bundled_position::handler(ctx, bundle_index);
}
}

View File

@ -10,7 +10,7 @@ use crate::{
math::{get_amount_delta_a, get_amount_delta_b, sqrt_price_from_tick_index},
state::*,
};
use anchor_lang::prelude::{AccountLoader, ProgramError};
use anchor_lang::prelude::{AccountLoader, *};
#[derive(Debug)]
pub struct ModifyLiquidityUpdate {
@ -31,7 +31,7 @@ pub fn calculate_modify_liquidity<'info>(
tick_array_upper: &AccountLoader<'info, TickArray>,
liquidity_delta: i128,
timestamp: u64,
) -> Result<ModifyLiquidityUpdate, ProgramError> {
) -> Result<ModifyLiquidityUpdate> {
let tick_array_lower = tick_array_lower.load()?;
let tick_lower =
tick_array_lower.get_tick(position.tick_lower_index, whirlpool.tick_spacing)?;
@ -58,7 +58,7 @@ pub fn calculate_fee_and_reward_growths<'info>(
tick_array_lower: &AccountLoader<'info, TickArray>,
tick_array_upper: &AccountLoader<'info, TickArray>,
timestamp: u64,
) -> Result<(PositionUpdate, [WhirlpoolRewardInfo; NUM_REWARDS]), ProgramError> {
) -> Result<(PositionUpdate, [WhirlpoolRewardInfo; NUM_REWARDS])> {
let tick_array_lower = tick_array_lower.load()?;
let tick_lower =
tick_array_lower.get_tick(position.tick_lower_index, whirlpool.tick_spacing)?;
@ -92,7 +92,7 @@ fn _calculate_modify_liquidity(
tick_upper_index: i32,
liquidity_delta: i128,
timestamp: u64,
) -> Result<ModifyLiquidityUpdate, ErrorCode> {
) -> Result<ModifyLiquidityUpdate> {
// Disallow only updating position fee and reward growth when position has zero liquidity
if liquidity_delta == 0 && position.liquidity == 0 {
return Err(ErrorCode::LiquidityZero.into());
@ -170,7 +170,7 @@ pub fn calculate_liquidity_token_deltas(
sqrt_price: u128,
position: &Position,
liquidity_delta: i128,
) -> Result<(u64, u64), ErrorCode> {
) -> Result<(u64, u64)> {
if liquidity_delta == 0 {
return Err(ErrorCode::LiquidityZero.into());
}
@ -206,7 +206,7 @@ pub fn sync_modify_liquidity_values<'info>(
tick_array_upper: &AccountLoader<'info, TickArray>,
modify_liquidity_update: ModifyLiquidityUpdate,
reward_last_updated_timestamp: u64,
) -> Result<(), ProgramError> {
) -> Result<()> {
position.update(&modify_liquidity_update.position_update);
tick_array_lower.load_mut()?.update_tick(

View File

@ -7,6 +7,7 @@ use crate::{
state::*,
util::SwapTickSequence,
};
use anchor_lang::prelude::*;
use std::convert::TryInto;
#[derive(Debug)]
@ -29,7 +30,7 @@ pub fn swap(
amount_specified_is_input: bool,
a_to_b: bool,
timestamp: u64,
) -> Result<PostSwapUpdate, ErrorCode> {
) -> Result<PostSwapUpdate> {
if sqrt_price_limit < MIN_SQRT_PRICE_X64 || sqrt_price_limit > MAX_SQRT_PRICE_X64 {
return Err(ErrorCode::SqrtPriceOutOfBounds.into());
}
@ -88,8 +89,8 @@ pub fn swap(
amount_remaining = amount_remaining
.checked_sub(swap_computation.amount_in)
.ok_or(ErrorCode::AmountRemainingOverflow)?;
amount_remaining = amount_remaining.
checked_sub(swap_computation.fee_amount)
amount_remaining = amount_remaining
.checked_sub(swap_computation.fee_amount)
.ok_or(ErrorCode::AmountRemainingOverflow)?;
amount_calculated = amount_calculated
@ -233,7 +234,7 @@ fn calculate_update(
fee_growth_global_a: u128,
fee_growth_global_b: u128,
reward_infos: &[WhirlpoolRewardInfo; NUM_REWARDS],
) -> Result<(TickUpdate, u128), ErrorCode> {
) -> Result<(TickUpdate, u128)> {
// Use updated fee_growth for crossing tick
// Use -liquidity_net if going left, +liquidity_net going right
let signed_liquidity_net = if a_to_b {
@ -2499,7 +2500,6 @@ mod swap_error_tests {
swap_test_info.run(&mut tick_sequence, 100);
}
#[test]
#[should_panic(expected = "AmountCalcOverflow")]
// Swapping at high liquidity/price can lead to an amount calculated
@ -2511,7 +2511,8 @@ mod swap_error_tests {
// Use filled arrays to minimize the the overflow from calculations, rather than accumulation
let array_1_ticks: Vec<TestTickInfo> = build_filled_tick_array(439296, TS_128);
let array_2_ticks: Vec<TestTickInfo> = build_filled_tick_array(439296 - 88 * 128, TS_128);
let array_3_ticks: Vec<TestTickInfo> = build_filled_tick_array(439296 - 2 * 88 * 128, TS_128);
let array_3_ticks: Vec<TestTickInfo> =
build_filled_tick_array(439296 - 2 * 88 * 128, TS_128);
let swap_test_info = SwapTestFixture::new(SwapTestFixtureInfo {
tick_spacing: TS_128,
liquidity: (u32::MAX as u128) << 2,
@ -2533,5 +2534,4 @@ mod swap_error_tests {
);
swap_test_info.run(&mut tick_sequence, 100);
}
}

View File

@ -57,11 +57,7 @@ pub fn checked_mul_shift_right_round_up_if(
return Err(ErrorCode::MultiplicationOverflow);
}
Ok(if should_round {
result + 1
} else {
result
})
Ok(if should_round { result + 1 } else { result })
}
pub fn div_round_up(n: u128, d: u128) -> Result<u128, ErrorCode> {
@ -112,8 +108,8 @@ mod fuzz_tests {
assert!(rounded.is_err());
} else {
let unrounded = n / d;
let div_unrounded = div_round_up_if(n, d, false)?;
let diff = rounded? - unrounded;
let div_unrounded = div_round_up_if(n, d, false).unwrap();
let diff = rounded.unwrap() - unrounded;
assert!(unrounded == div_unrounded);
assert!(diff <= 1);
assert!((diff == 1) == (n % d > 0));
@ -143,7 +139,7 @@ mod fuzz_tests {
let other_remainder = other_dividend % other_divisor;
let unrounded = div_round_up_if_u256(dividend, divisor, false);
assert!(unrounded? == other_quotient.try_into_u128()?);
assert!(unrounded.unwrap() == other_quotient.try_into_u128().unwrap());
let diff = rounded.unwrap() - unrounded.unwrap();
assert!(diff <= 1);
@ -166,7 +162,7 @@ mod fuzz_tests {
let other_d = U256::from(d);
let other_result = other_p / other_d;
let unrounded = checked_mul_div_round_up_if(n0, n1, d, false)?;
let unrounded = checked_mul_div_round_up_if(n0, n1, d, false).unwrap();
assert!(U256::from(unrounded) == other_result);
let diff = U256::from(result.unwrap()) - other_result;
@ -182,12 +178,12 @@ mod fuzz_tests {
if n0.checked_mul(n1).is_none() {
assert!(result.is_err());
} else {
let p = (U256::from(n0) * U256::from(n1)).try_into_u128()?;
let p = (U256::from(n0) * U256::from(n1)).try_into_u128().unwrap();
let i = (p >> 64) as u64;
assert!(i == checked_mul_shift_right_round_up_if(n0, n1, false)?);
assert!(i == checked_mul_shift_right_round_up_if(n0, n1, false).unwrap());
if i == u64::MAX && (p & Q64_MASK > 0) {
assert!(result.is_err());
@ -315,7 +311,6 @@ mod test_bit_math {
);
assert_eq!(div_round_up(u128::MAX - 1, u128::MAX).unwrap(), 1);
}
}
mod test_mult_shift_right_round_up {
@ -323,21 +318,59 @@ mod test_bit_math {
#[test]
fn test_mul_shift_right_ok() {
assert_eq!(checked_mul_shift_right_round_up_if(u64::MAX as u128, 1, false).unwrap(), 0);
assert_eq!(checked_mul_shift_right_round_up_if(u64::MAX as u128, 1, true).unwrap(), 1);
assert_eq!(checked_mul_shift_right_round_up_if(u64::MAX as u128 + 1, 1, false).unwrap(), 1);
assert_eq!(checked_mul_shift_right_round_up_if(u64::MAX as u128 + 1, 1, true).unwrap(), 1);
assert_eq!(checked_mul_shift_right_round_up_if(u32::MAX as u128, u32::MAX as u128, false).unwrap(), 0);
assert_eq!(checked_mul_shift_right_round_up_if(u32::MAX as u128, u32::MAX as u128, true).unwrap(), 1);
assert_eq!(checked_mul_shift_right_round_up_if(u32::MAX as u128 + 1, u32::MAX as u128 + 2, false).unwrap(), 1);
assert_eq!(checked_mul_shift_right_round_up_if(u32::MAX as u128 + 1, u32::MAX as u128 + 2, true).unwrap(), 2);
assert_eq!(
checked_mul_shift_right_round_up_if(u64::MAX as u128, 1, false).unwrap(),
0
);
assert_eq!(
checked_mul_shift_right_round_up_if(u64::MAX as u128, 1, true).unwrap(),
1
);
assert_eq!(
checked_mul_shift_right_round_up_if(u64::MAX as u128 + 1, 1, false).unwrap(),
1
);
assert_eq!(
checked_mul_shift_right_round_up_if(u64::MAX as u128 + 1, 1, true).unwrap(),
1
);
assert_eq!(
checked_mul_shift_right_round_up_if(u32::MAX as u128, u32::MAX as u128, false)
.unwrap(),
0
);
assert_eq!(
checked_mul_shift_right_round_up_if(u32::MAX as u128, u32::MAX as u128, true)
.unwrap(),
1
);
assert_eq!(
checked_mul_shift_right_round_up_if(
u32::MAX as u128 + 1,
u32::MAX as u128 + 2,
false
)
.unwrap(),
1
);
assert_eq!(
checked_mul_shift_right_round_up_if(
u32::MAX as u128 + 1,
u32::MAX as u128 + 2,
true
)
.unwrap(),
2
);
}
#[test]
fn test_mul_shift_right_u64_max() {
assert!(checked_mul_shift_right_round_up_if(u128::MAX, 1, true).is_err());
assert_eq!(checked_mul_shift_right_round_up_if(u128::MAX, 1, false).unwrap(), u64::MAX);
assert_eq!(
checked_mul_shift_right_round_up_if(u128::MAX, 1, false).unwrap(),
u64::MAX
);
}
}
}

View File

@ -307,7 +307,7 @@ mod fuzz_tests {
if liquidity.leading_zeros() + sqrt_price.leading_zeros() < Q64_RESOLUTION.into() {
assert!(case_1_price.is_err());
} else {
assert!(amount >= get_amount_delta_a(sqrt_price, case_1_price?, liquidity, true)?);
assert!(amount >= get_amount_delta_a(sqrt_price, case_1_price.unwrap(), liquidity, true).unwrap());
// Case 2. amount_specified_is_input = false, a_to_b = false
// We are removing token A from the supply, causing price to increase (Eq 1.)
@ -327,12 +327,12 @@ mod fuzz_tests {
if liquidity_x64 <= product {
assert!(case_2_price.is_err());
} else {
assert!(amount <= get_amount_delta_a(sqrt_price, case_2_price?, liquidity, false)?);
assert!(case_2_price? >= sqrt_price);
assert!(amount <= get_amount_delta_a(sqrt_price, case_2_price.unwrap(), liquidity, false).unwrap());
assert!(case_2_price.unwrap() >= sqrt_price);
}
if amount == 0 {
assert!(case_1_price? == case_2_price?);
assert!(case_1_price.unwrap() == case_2_price.unwrap());
}
}
}
@ -351,9 +351,9 @@ mod fuzz_tests {
// Because a lower price is inversely correlated with an increased supply of B,
// a lower price means that we are adding less B. Thus when performing math, we wish to round the
// price down, since that means that we are guaranteed to not exceed the fixed amount of B provided.
let case_3_price = get_next_sqrt_price_from_b_round_down(sqrt_price, liquidity, amount, true)?;
let case_3_price = get_next_sqrt_price_from_b_round_down(sqrt_price, liquidity, amount, true).unwrap();
assert!(case_3_price >= sqrt_price);
assert!(amount >= get_amount_delta_b(sqrt_price, case_3_price, liquidity, true)?);
assert!(amount >= get_amount_delta_b(sqrt_price, case_3_price, liquidity, true).unwrap());
// Case 4. amount_specified_is_input = false, a_to_b = true
// We are removing token B from the supply, causing price to decrease (Eq 1.)
@ -365,22 +365,22 @@ mod fuzz_tests {
// Q64.0 << 64 => Q64.64
let amount_x64 = u128::from(amount) << Q64_RESOLUTION;
let delta = div_round_up(amount_x64, liquidity.into())?;
let delta = div_round_up(amount_x64, liquidity.into()).unwrap();
if sqrt_price < delta {
// In Case 4, error if sqrt_price < delta
assert!(case_4_price.is_err());
} else {
let calc_delta = get_amount_delta_b(sqrt_price, case_4_price?, liquidity, false);
let calc_delta = get_amount_delta_b(sqrt_price, case_4_price.unwrap(), liquidity, false);
if calc_delta.is_ok() {
assert!(amount <= calc_delta?);
assert!(amount <= calc_delta.unwrap());
}
// In Case 4, price is decreasing
assert!(case_4_price? <= sqrt_price);
assert!(case_4_price.unwrap() <= sqrt_price);
}
if amount == 0 {
assert!(case_3_price == case_4_price?);
assert!(case_3_price == case_4_price.unwrap());
}
}
@ -398,17 +398,17 @@ mod fuzz_tests {
if liquidity.leading_zeros() + (sqrt_price_upper - sqrt_price_lower).leading_zeros() < Q64_RESOLUTION.into() {
assert!(rounded.is_err())
} else {
let unrounded = get_amount_delta_a(sqrt_price_0, sqrt_price_1, liquidity, false)?;
let unrounded = get_amount_delta_a(sqrt_price_0, sqrt_price_1, liquidity, false).unwrap();
// Price difference symmetry
assert_eq!(rounded?, get_amount_delta_a(sqrt_price_1, sqrt_price_0, liquidity, true)?);
assert_eq!(unrounded, get_amount_delta_a(sqrt_price_1, sqrt_price_0, liquidity, false)?);
assert_eq!(rounded.unwrap(), get_amount_delta_a(sqrt_price_1, sqrt_price_0, liquidity, true).unwrap());
assert_eq!(unrounded, get_amount_delta_a(sqrt_price_1, sqrt_price_0, liquidity, false).unwrap());
// Rounded should always be larger
assert!(unrounded <= rounded?);
assert!(unrounded <= rounded.unwrap());
// Diff should be no more than 1
assert!(rounded? - unrounded <= 1);
assert!(rounded.unwrap() - unrounded <= 1);
}
}
@ -440,17 +440,17 @@ mod fuzz_tests {
} else if round_up_delta > u64_max_in_u256 {
assert!(rounded.is_err());
// Price symmmetry
assert_eq!(unrounded?, get_amount_delta_b(sqrt_price_1, sqrt_price_0, liquidity, false)?);
assert_eq!(unrounded.unwrap(), get_amount_delta_b(sqrt_price_1, sqrt_price_0, liquidity, false).unwrap());
} else {
// Price difference symmetry
assert_eq!(rounded?, get_amount_delta_b(sqrt_price_1, sqrt_price_0, liquidity, true)?);
assert_eq!(unrounded?, get_amount_delta_b(sqrt_price_1, sqrt_price_0, liquidity, false)?);
assert_eq!(rounded.unwrap(), get_amount_delta_b(sqrt_price_1, sqrt_price_0, liquidity, true).unwrap());
assert_eq!(unrounded.unwrap(), get_amount_delta_b(sqrt_price_1, sqrt_price_0, liquidity, false).unwrap());
// Rounded should always be larger
assert!(unrounded? <= rounded? );
assert!(unrounded.unwrap() <= rounded.unwrap());
// Diff should be no more than 1
assert!(rounded? - unrounded? <= 1);
assert!(rounded.unwrap() - unrounded.unwrap() <= 1);
}
}

View File

@ -31,7 +31,7 @@ impl WhirlpoolsConfig {
collect_protocol_fees_authority: Pubkey,
reward_emissions_super_authority: Pubkey,
default_protocol_fee_rate: u16,
) -> Result<(), ErrorCode> {
) -> Result<()> {
self.fee_authority = fee_authority;
self.collect_protocol_fees_authority = collect_protocol_fees_authority;
self.reward_emissions_super_authority = reward_emissions_super_authority;
@ -50,7 +50,7 @@ impl WhirlpoolsConfig {
pub fn update_default_protocol_fee_rate(
&mut self,
default_protocol_fee_rate: u16,
) -> Result<(), ErrorCode> {
) -> Result<()> {
if default_protocol_fee_rate > MAX_PROTOCOL_FEE_RATE {
return Err(ErrorCode::ProtocolFeeRateMaxExceeded.into());
}

View File

@ -17,14 +17,14 @@ impl FeeTier {
whirlpools_config: &Account<WhirlpoolsConfig>,
tick_spacing: u16,
default_fee_rate: u16,
) -> Result<(), ErrorCode> {
) -> Result<()> {
self.whirlpools_config = whirlpools_config.key();
self.tick_spacing = tick_spacing;
self.update_default_fee_rate(default_fee_rate)?;
Ok(())
}
pub fn update_default_fee_rate(&mut self, default_fee_rate: u16) -> Result<(), ErrorCode> {
pub fn update_default_fee_rate(&mut self, default_fee_rate: u16) -> Result<()> {
if default_fee_rate > MAX_FEE_RATE {
return Err(ErrorCode::FeeRateMaxExceeded.into());
}

View File

@ -1,6 +1,7 @@
pub mod config;
pub mod fee_tier;
pub mod position;
pub mod position_bundle;
pub mod tick;
pub mod whirlpool;
@ -8,4 +9,5 @@ pub use self::whirlpool::*;
pub use config::*;
pub use fee_tier::*;
pub use position::*;
pub use position_bundle::*;
pub use tick::*;

View File

@ -61,7 +61,7 @@ impl Position {
position_mint: Pubkey,
tick_lower_index: i32,
tick_upper_index: i32,
) -> Result<(), ErrorCode> {
) -> Result<()> {
if !Tick::check_is_usable_tick(tick_lower_index, whirlpool.tick_spacing)
|| !Tick::check_is_usable_tick(tick_upper_index, whirlpool.tick_spacing)
|| tick_lower_index >= tick_upper_index

View File

@ -0,0 +1,270 @@
use crate::errors::ErrorCode;
use anchor_lang::prelude::*;
pub const POSITION_BITMAP_USIZE: usize = 32;
pub const POSITION_BUNDLE_SIZE: u16 = 8 * POSITION_BITMAP_USIZE as u16;
#[account]
#[derive(Default)]
pub struct PositionBundle {
pub position_bundle_mint: Pubkey, // 32
pub position_bitmap: [u8; POSITION_BITMAP_USIZE], // 32
// 64 RESERVE
}
impl PositionBundle {
pub const LEN: usize = 8 + 32 + 32 + 64;
pub fn initialize(&mut self, position_bundle_mint: Pubkey) -> Result<()> {
self.position_bundle_mint = position_bundle_mint;
// position_bitmap is initialized using Default trait
Ok(())
}
pub fn is_deletable(&self) -> bool {
for bitmap in self.position_bitmap.iter() {
if *bitmap != 0 {
return false;
}
}
true
}
pub fn open_bundled_position(&mut self, bundle_index: u16) -> Result<()> {
self.update_bitmap(bundle_index, true)
}
pub fn close_bundled_position(&mut self, bundle_index: u16) -> Result<()> {
self.update_bitmap(bundle_index, false)
}
fn update_bitmap(&mut self, bundle_index: u16, open: bool) -> Result<()> {
if !PositionBundle::is_valid_bundle_index(bundle_index) {
return Err(ErrorCode::InvalidBundleIndex.into());
}
let bitmap_index = bundle_index / 8;
let bitmap_offset = bundle_index % 8;
let bitmap = self.position_bitmap[bitmap_index as usize];
let mask = 1 << bitmap_offset;
let bit = bitmap & mask;
let opened = bit != 0;
if open && opened {
// UNREACHABLE
// Anchor should reject with AccountDiscriminatorAlreadySet
return Err(ErrorCode::BundledPositionAlreadyOpened.into());
}
if !open && !opened {
// UNREACHABLE
// Anchor should reject with AccountNotInitialized
return Err(ErrorCode::BundledPositionAlreadyClosed.into());
}
let updated_bitmap = bitmap ^ mask;
self.position_bitmap[bitmap_index as usize] = updated_bitmap;
Ok(())
}
fn is_valid_bundle_index(bundle_index: u16) -> bool {
bundle_index < POSITION_BUNDLE_SIZE
}
}
#[cfg(test)]
mod position_bundle_initialize_tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_default() {
let position_bundle = PositionBundle {
..Default::default()
};
assert_eq!(position_bundle.position_bundle_mint, Pubkey::default());
for bitmap in position_bundle.position_bitmap.iter() {
assert_eq!(*bitmap, 0);
}
}
#[test]
fn test_initialize() {
let mut position_bundle = PositionBundle {
..Default::default()
};
let position_bundle_mint =
Pubkey::from_str("orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE").unwrap();
let result = position_bundle.initialize(position_bundle_mint);
assert!(result.is_ok());
assert_eq!(position_bundle.position_bundle_mint, position_bundle_mint);
for bitmap in position_bundle.position_bitmap.iter() {
assert_eq!(*bitmap, 0);
}
}
}
#[cfg(test)]
mod position_bundle_is_deletable_tests {
use super::*;
#[test]
fn test_default_is_deletable() {
let position_bundle = PositionBundle {
..Default::default()
};
assert!(position_bundle.is_deletable());
}
#[test]
fn test_each_bit_detectable() {
let mut position_bundle = PositionBundle {
..Default::default()
};
for bundle_index in 0..POSITION_BUNDLE_SIZE {
let index = bundle_index / 8;
let offset = bundle_index % 8;
position_bundle.position_bitmap[index as usize] = 1 << offset;
assert!(!position_bundle.is_deletable());
position_bundle.position_bitmap[index as usize] = 0;
assert!(position_bundle.is_deletable());
}
}
}
#[cfg(test)]
mod position_bundle_open_and_close_tests {
use super::*;
#[test]
fn test_open_and_close_zero() {
let mut position_bundle = PositionBundle {
..Default::default()
};
let r1 = position_bundle.open_bundled_position(0);
assert!(r1.is_ok());
assert_eq!(position_bundle.position_bitmap[0], 1);
let r2 = position_bundle.close_bundled_position(0);
assert!(r2.is_ok());
assert_eq!(position_bundle.position_bitmap[0], 0);
}
#[test]
fn test_open_and_close_middle() {
let mut position_bundle = PositionBundle {
..Default::default()
};
let r1 = position_bundle.open_bundled_position(130);
assert!(r1.is_ok());
assert_eq!(position_bundle.position_bitmap[16], 4);
let r2 = position_bundle.close_bundled_position(130);
assert!(r2.is_ok());
assert_eq!(position_bundle.position_bitmap[16], 0);
}
#[test]
fn test_open_and_close_max() {
let mut position_bundle = PositionBundle {
..Default::default()
};
let r1 = position_bundle.open_bundled_position(POSITION_BUNDLE_SIZE - 1);
assert!(r1.is_ok());
assert_eq!(
position_bundle.position_bitmap[POSITION_BITMAP_USIZE - 1],
128
);
let r2 = position_bundle.close_bundled_position(POSITION_BUNDLE_SIZE - 1);
assert!(r2.is_ok());
assert_eq!(
position_bundle.position_bitmap[POSITION_BITMAP_USIZE - 1],
0
);
}
#[test]
fn test_double_open_should_be_failed() {
let mut position_bundle = PositionBundle {
..Default::default()
};
let r1 = position_bundle.open_bundled_position(0);
assert!(r1.is_ok());
let r2 = position_bundle.open_bundled_position(0);
assert!(r2.is_err());
}
#[test]
fn test_double_close_should_be_failed() {
let mut position_bundle = PositionBundle {
..Default::default()
};
let r1 = position_bundle.open_bundled_position(0);
assert!(r1.is_ok());
let r2 = position_bundle.close_bundled_position(0);
assert!(r2.is_ok());
let r3 = position_bundle.close_bundled_position(0);
assert!(r3.is_err());
}
#[test]
fn test_all_open_and_all_close() {
let mut position_bundle = PositionBundle {
..Default::default()
};
for bundle_index in 0..POSITION_BUNDLE_SIZE {
let r = position_bundle.open_bundled_position(bundle_index);
assert!(r.is_ok());
}
for bitmap in position_bundle.position_bitmap.iter() {
assert_eq!(*bitmap, 255);
}
for bundle_index in 0..POSITION_BUNDLE_SIZE {
let r = position_bundle.close_bundled_position(bundle_index);
assert!(r.is_ok());
}
for bitmap in position_bundle.position_bitmap.iter() {
assert_eq!(*bitmap, 0);
}
}
#[test]
fn test_open_bundle_index_out_of_bounds() {
let mut position_bundle = PositionBundle {
..Default::default()
};
for bundle_index in POSITION_BUNDLE_SIZE..u16::MAX {
let r = position_bundle.open_bundled_position(bundle_index);
assert!(r.is_err());
}
}
#[test]
fn test_close_bundle_index_out_of_bounds() {
let mut position_bundle = PositionBundle {
..Default::default()
};
for bundle_index in POSITION_BUNDLE_SIZE..u16::MAX {
let r = position_bundle.close_bundled_position(bundle_index);
assert!(r.is_err());
}
}
}

View File

@ -178,9 +178,9 @@ impl TickArray {
tick_index: i32,
tick_spacing: u16,
a_to_b: bool,
) -> Result<Option<i32>, ErrorCode> {
) -> Result<Option<i32>> {
if !self.in_search_range(tick_index, tick_spacing, !a_to_b) {
return Err(ErrorCode::InvalidTickArraySequence);
return Err(ErrorCode::InvalidTickArraySequence.into());
}
let mut curr_offset = match self.tick_offset(tick_index, tick_spacing) {
@ -224,7 +224,7 @@ impl TickArray {
&mut self,
whirlpool: &Account<Whirlpool>,
start_tick_index: i32,
) -> Result<(), ErrorCode> {
) -> Result<()> {
if !Tick::check_is_valid_start_tick(start_tick_index, whirlpool.tick_spacing) {
return Err(ErrorCode::InvalidStartTick.into());
}
@ -243,15 +243,15 @@ impl TickArray {
/// # Returns
/// - `&Tick`: A reference to the desired Tick object
/// - `TickNotFound`: - The provided tick-index is not an initializable tick index in this Whirlpool w/ this tick-spacing.
pub fn get_tick(&self, tick_index: i32, tick_spacing: u16) -> Result<&Tick, ErrorCode> {
pub fn get_tick(&self, tick_index: i32, tick_spacing: u16) -> Result<&Tick> {
if !self.check_in_array_bounds(tick_index, tick_spacing)
|| !Tick::check_is_usable_tick(tick_index, tick_spacing)
{
return Err(ErrorCode::TickNotFound);
return Err(ErrorCode::TickNotFound.into());
}
let offset = self.tick_offset(tick_index, tick_spacing)?;
if offset < 0 {
return Err(ErrorCode::TickNotFound);
return Err(ErrorCode::TickNotFound.into());
}
Ok(&self.ticks[offset as usize])
}
@ -270,15 +270,15 @@ impl TickArray {
tick_index: i32,
tick_spacing: u16,
update: &TickUpdate,
) -> Result<(), ErrorCode> {
) -> Result<()> {
if !self.check_in_array_bounds(tick_index, tick_spacing)
|| !Tick::check_is_usable_tick(tick_index, tick_spacing)
{
return Err(ErrorCode::TickNotFound);
return Err(ErrorCode::TickNotFound.into());
}
let offset = self.tick_offset(tick_index, tick_spacing)?;
if offset < 0 {
return Err(ErrorCode::TickNotFound);
return Err(ErrorCode::TickNotFound.into());
}
self.ticks.get_mut(offset as usize).unwrap().update(update);
Ok(())
@ -319,9 +319,9 @@ impl TickArray {
}
// Calculates an offset from a tick index that can be used to access the tick data
pub fn tick_offset(&self, tick_index: i32, tick_spacing: u16) -> Result<isize, ErrorCode> {
pub fn tick_offset(&self, tick_index: i32, tick_spacing: u16) -> Result<isize> {
if tick_spacing == 0 {
return Err(ErrorCode::InvalidTickSpacing);
return Err(ErrorCode::InvalidTickSpacing.into());
}
Ok(get_offset(tick_index, self.start_tick_index, tick_spacing))

View File

@ -80,7 +80,7 @@ impl Whirlpool {
token_vault_a: Pubkey,
token_mint_b: Pubkey,
token_vault_b: Pubkey,
) -> Result<(), ErrorCode> {
) -> Result<()> {
if token_mint_a.ge(&token_mint_b) {
return Err(ErrorCode::InvalidTokenMintOrder.into());
}
@ -145,11 +145,7 @@ impl Whirlpool {
}
/// Update the reward authority at the specified Whirlpool reward index.
pub fn update_reward_authority(
&mut self,
index: usize,
authority: Pubkey,
) -> Result<(), ErrorCode> {
pub fn update_reward_authority(&mut self, index: usize, authority: Pubkey) -> Result<()> {
if index >= NUM_REWARDS {
return Err(ErrorCode::InvalidRewardIndex.into());
}
@ -164,7 +160,7 @@ impl Whirlpool {
reward_infos: [WhirlpoolRewardInfo; NUM_REWARDS],
timestamp: u64,
emissions_per_second_x64: u128,
) -> Result<(), ErrorCode> {
) -> Result<()> {
if index >= NUM_REWARDS {
return Err(ErrorCode::InvalidRewardIndex.into());
}
@ -174,12 +170,7 @@ impl Whirlpool {
Ok(())
}
pub fn initialize_reward(
&mut self,
index: usize,
mint: Pubkey,
vault: Pubkey,
) -> Result<(), ErrorCode> {
pub fn initialize_reward(&mut self, index: usize, mint: Pubkey, vault: Pubkey) -> Result<()> {
if index >= NUM_REWARDS {
return Err(ErrorCode::InvalidRewardIndex.into());
}
@ -226,7 +217,7 @@ impl Whirlpool {
}
}
pub fn update_fee_rate(&mut self, fee_rate: u16) -> Result<(), ErrorCode> {
pub fn update_fee_rate(&mut self, fee_rate: u16) -> Result<()> {
if fee_rate > MAX_FEE_RATE {
return Err(ErrorCode::FeeRateMaxExceeded.into());
}
@ -235,7 +226,7 @@ impl Whirlpool {
Ok(())
}
pub fn update_protocol_fee_rate(&mut self, protocol_fee_rate: u16) -> Result<(), ErrorCode> {
pub fn update_protocol_fee_rate(&mut self, protocol_fee_rate: u16) -> Result<()> {
if protocol_fee_rate > MAX_PROTOCOL_FEE_RATE {
return Err(ErrorCode::ProtocolFeeRateMaxExceeded.into());
}

View File

@ -149,7 +149,9 @@ fn run_swap_integration_tests() {
msg!("");
fail_cases += 1;
} else if expected_error.is_some() && !expected_error.unwrap().eq(&e) {
} else if expected_error.is_some()
&& !anchor_lang::error!(expected_error.unwrap()).eq(&e)
{
fail_cases += 1;
msg!("Test case {} - {}", test_id, test.description);

View File

@ -1,5 +1,6 @@
use crate::errors::ErrorCode;
use crate::state::*;
use anchor_lang::prelude::*;
use std::cell::RefMut;
pub struct SwapTickSequence<'info> {
@ -39,11 +40,11 @@ impl<'info> SwapTickSequence<'info> {
array_index: usize,
tick_index: i32,
tick_spacing: u16,
) -> Result<&Tick, ErrorCode> {
) -> Result<&Tick> {
let array = self.arrays.get(array_index);
match array {
Some(array) => array.get_tick(tick_index, tick_spacing),
_ => Err(ErrorCode::TickArrayIndexOutofBounds),
_ => Err(ErrorCode::TickArrayIndexOutofBounds.into()),
}
}
@ -64,14 +65,14 @@ impl<'info> SwapTickSequence<'info> {
tick_index: i32,
tick_spacing: u16,
update: &TickUpdate,
) -> Result<(), ErrorCode> {
) -> Result<()> {
let array = self.arrays.get_mut(array_index);
match array {
Some(array) => {
array.update_tick(tick_index, tick_spacing, update)?;
Ok(())
}
_ => Err(ErrorCode::TickArrayIndexOutofBounds),
_ => Err(ErrorCode::TickArrayIndexOutofBounds.into()),
}
}
@ -80,11 +81,11 @@ impl<'info> SwapTickSequence<'info> {
array_index: usize,
tick_index: i32,
tick_spacing: u16,
) -> Result<isize, ErrorCode> {
) -> Result<isize> {
let array = self.arrays.get(array_index);
match array {
Some(array) => array.tick_offset(tick_index, tick_spacing),
_ => Err(ErrorCode::TickArrayIndexOutofBounds),
_ => Err(ErrorCode::TickArrayIndexOutofBounds.into()),
}
}
@ -108,7 +109,7 @@ impl<'info> SwapTickSequence<'info> {
tick_spacing: u16,
a_to_b: bool,
start_array_index: usize,
) -> Result<(usize, i32), ErrorCode> {
) -> Result<(usize, i32)> {
let ticks_in_array = TICK_ARRAY_SIZE * tick_spacing as i32;
let mut search_index = tick_index;
let mut array_index = start_array_index;
@ -118,7 +119,7 @@ impl<'info> SwapTickSequence<'info> {
// If we get to the end of the array sequence and next_index is still not found, throw error
let next_array = match self.arrays.get(array_index) {
Some(array) => array,
None => return Err(ErrorCode::TickArraySequenceInvalidIndex),
None => return Err(ErrorCode::TickArraySequenceInvalidIndex.into()),
};
let next_index =
@ -250,7 +251,7 @@ mod swap_tick_sequence_tests {
TS_128,
);
assert_eq!(result.unwrap_err(), ErrorCode::TickNotFound);
assert_eq!(result.unwrap_err(), ErrorCode::TickNotFound.into());
let update_result = swap_tick_sequence.update_tick(
uninitializable_search_tick.0,
@ -262,7 +263,7 @@ mod swap_tick_sequence_tests {
..Default::default()
},
);
assert_eq!(update_result.unwrap_err(), ErrorCode::TickNotFound);
assert_eq!(update_result.unwrap_err(), ErrorCode::TickNotFound.into());
}
}
@ -327,7 +328,7 @@ mod swap_tick_sequence_tests {
let get_result = swap_tick_sequence.get_tick(3, 5000, TS_128);
assert_eq!(
get_result.unwrap_err(),
ErrorCode::TickArrayIndexOutofBounds
ErrorCode::TickArrayIndexOutofBounds.into()
);
let update_result = swap_tick_sequence.update_tick(
@ -340,7 +341,7 @@ mod swap_tick_sequence_tests {
);
assert_eq!(
update_result.unwrap_err(),
ErrorCode::TickArrayIndexOutofBounds
ErrorCode::TickArrayIndexOutofBounds.into()
);
}
}

View File

@ -1,9 +1,7 @@
use anchor_lang::prelude::*;
use anchor_spl::token::{TokenAccount, Token};
use anchor_spl::token::{Token, TokenAccount};
use crate::{
manager::swap_manager::PostSwapUpdate, state::Whirlpool
};
use crate::{manager::swap_manager::PostSwapUpdate, state::Whirlpool};
use super::{transfer_from_owner_to_vault, transfer_from_vault_to_owner};
@ -18,7 +16,7 @@ pub fn update_and_swap_whirlpool<'info>(
swap_update: PostSwapUpdate,
is_token_fee_in_a: bool,
reward_last_updated_timestamp: u64,
) -> ProgramResult {
) -> Result<()> {
whirlpool.update_after_swap(
swap_update.next_liquidity,
swap_update.next_tick_index,
@ -45,60 +43,60 @@ pub fn update_and_swap_whirlpool<'info>(
}
fn perform_swap<'info>(
whirlpool: &Account<'info, Whirlpool>,
token_authority: &Signer<'info>,
token_owner_account_a: &Account<'info, TokenAccount>,
token_owner_account_b: &Account<'info, TokenAccount>,
token_vault_a: &Account<'info, TokenAccount>,
token_vault_b: &Account<'info, TokenAccount>,
token_program: &Program<'info, Token>,
amount_a: u64,
amount_b: u64,
a_to_b: bool,
) -> ProgramResult {
// Transfer from user to pool
let deposit_account_user;
let deposit_account_pool;
let deposit_amount;
whirlpool: &Account<'info, Whirlpool>,
token_authority: &Signer<'info>,
token_owner_account_a: &Account<'info, TokenAccount>,
token_owner_account_b: &Account<'info, TokenAccount>,
token_vault_a: &Account<'info, TokenAccount>,
token_vault_b: &Account<'info, TokenAccount>,
token_program: &Program<'info, Token>,
amount_a: u64,
amount_b: u64,
a_to_b: bool,
) -> Result<()> {
// Transfer from user to pool
let deposit_account_user;
let deposit_account_pool;
let deposit_amount;
// Transfer from pool to user
let withdrawal_account_user;
let withdrawal_account_pool;
let withdrawal_amount;
// Transfer from pool to user
let withdrawal_account_user;
let withdrawal_account_pool;
let withdrawal_amount;
if a_to_b {
deposit_account_user = token_owner_account_a;
deposit_account_pool = token_vault_a;
deposit_amount = amount_a;
if a_to_b {
deposit_account_user = token_owner_account_a;
deposit_account_pool = token_vault_a;
deposit_amount = amount_a;
withdrawal_account_user = token_owner_account_b;
withdrawal_account_pool = token_vault_b;
withdrawal_amount = amount_b;
} else {
deposit_account_user = token_owner_account_b;
deposit_account_pool = token_vault_b;
deposit_amount = amount_b;
withdrawal_account_user = token_owner_account_b;
withdrawal_account_pool = token_vault_b;
withdrawal_amount = amount_b;
} else {
deposit_account_user = token_owner_account_b;
deposit_account_pool = token_vault_b;
deposit_amount = amount_b;
withdrawal_account_user = token_owner_account_a;
withdrawal_account_pool = token_vault_a;
withdrawal_amount = amount_a;
}
withdrawal_account_user = token_owner_account_a;
withdrawal_account_pool = token_vault_a;
withdrawal_amount = amount_a;
}
transfer_from_owner_to_vault(
token_authority,
deposit_account_user,
deposit_account_pool,
token_program,
deposit_amount,
)?;
transfer_from_owner_to_vault(
token_authority,
deposit_account_user,
deposit_account_pool,
token_program,
deposit_amount,
)?;
transfer_from_vault_to_owner(
whirlpool,
withdrawal_account_pool,
withdrawal_account_user,
token_program,
withdrawal_amount,
)?;
transfer_from_vault_to_owner(
whirlpool,
withdrawal_account_pool,
withdrawal_account_user,
token_program,
withdrawal_amount,
)?;
Ok(())
Ok(())
}

View File

@ -1,4 +1,3 @@
use crate::errors::ErrorCode;
use crate::manager::swap_manager::*;
use crate::math::tick_math::*;
use crate::state::{
@ -222,7 +221,7 @@ impl SwapTestFixture {
&self,
tick_sequence: &mut SwapTickSequence,
next_timestamp: u64,
) -> Result<PostSwapUpdate, ErrorCode> {
) -> Result<PostSwapUpdate> {
swap(
&self.whirlpool,
tick_sequence,

View File

@ -1,17 +1,22 @@
use crate::state::Whirlpool;
use crate::state::{PositionBundle, Whirlpool};
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Mint, Token, TokenAccount, Transfer};
use mpl_token_metadata::instruction::create_metadata_accounts_v2;
use mpl_token_metadata::instruction::create_metadata_accounts_v3;
use solana_program::program::invoke_signed;
use spl_token::instruction::{burn_checked, close_account, mint_to, set_authority, AuthorityType};
use crate::constants::nft::{
WPB_METADATA_NAME_PREFIX, WPB_METADATA_SYMBOL, WPB_METADATA_URI, WP_METADATA_NAME,
WP_METADATA_SYMBOL, WP_METADATA_URI,
};
pub fn transfer_from_owner_to_vault<'info>(
position_authority: &Signer<'info>,
token_owner_account: &Account<'info, TokenAccount>,
token_vault: &Account<'info, TokenAccount>,
token_program: &Program<'info, Token>,
amount: u64,
) -> Result<(), ProgramError> {
) -> Result<()> {
token::transfer(
CpiContext::new(
token_program.to_account_info(),
@ -31,7 +36,7 @@ pub fn transfer_from_vault_to_owner<'info>(
token_owner_account: &Account<'info, TokenAccount>,
token_program: &Program<'info, Token>,
amount: u64,
) -> Result<(), ProgramError> {
) -> Result<()> {
token::transfer(
CpiContext::new_with_signer(
token_program.to_account_info(),
@ -52,7 +57,7 @@ pub fn burn_and_close_user_position_token<'info>(
position_mint: &Account<'info, Mint>,
position_token_account: &Account<'info, TokenAccount>,
token_program: &Program<'info, Token>,
) -> ProgramResult {
) -> Result<()> {
// Burn a single token in user account
invoke_signed(
&burn_checked(
@ -89,7 +94,8 @@ pub fn burn_and_close_user_position_token<'info>(
token_authority.to_account_info(),
],
&[],
)
)?;
Ok(())
}
pub fn mint_position_token_and_remove_authority<'info>(
@ -97,7 +103,7 @@ pub fn mint_position_token_and_remove_authority<'info>(
position_mint: &Account<'info, Mint>,
position_token_account: &Account<'info, TokenAccount>,
token_program: &Program<'info, Token>,
) -> ProgramResult {
) -> Result<()> {
mint_position_token(
whirlpool,
position_mint,
@ -107,10 +113,6 @@ pub fn mint_position_token_and_remove_authority<'info>(
remove_position_token_mint_authority(whirlpool, position_mint, token_program)
}
const WP_METADATA_NAME: &str = "Orca Whirlpool Position";
const WP_METADATA_SYMBOL: &str = "OWP";
const WP_METADATA_URI: &str = "https://arweave.net/E19ZNY2sqMqddm1Wx7mrXPUZ0ZZ5ISizhebb0UsVEws";
pub fn mint_position_token_with_metadata_and_remove_authority<'info>(
whirlpool: &Account<'info, Whirlpool>,
position_mint: &Account<'info, Mint>,
@ -122,7 +124,7 @@ pub fn mint_position_token_with_metadata_and_remove_authority<'info>(
token_program: &Program<'info, Token>,
system_program: &Program<'info, System>,
rent: &Sysvar<'info, Rent>,
) -> ProgramResult {
) -> Result<()> {
mint_position_token(
whirlpool,
position_mint,
@ -132,7 +134,7 @@ pub fn mint_position_token_with_metadata_and_remove_authority<'info>(
let metadata_mint_auth_account = whirlpool;
invoke_signed(
&create_metadata_accounts_v2(
&create_metadata_accounts_v3(
metadata_program.key(),
position_metadata_account.key(),
position_mint.key(),
@ -148,6 +150,7 @@ pub fn mint_position_token_with_metadata_and_remove_authority<'info>(
true,
None,
None,
None,
),
&[
position_metadata_account.to_account_info(),
@ -170,7 +173,7 @@ fn mint_position_token<'info>(
position_mint: &Account<'info, Mint>,
position_token_account: &Account<'info, TokenAccount>,
token_program: &Program<'info, Token>,
) -> ProgramResult {
) -> Result<()> {
invoke_signed(
&mint_to(
token_program.key,
@ -187,14 +190,15 @@ fn mint_position_token<'info>(
token_program.to_account_info(),
],
&[&whirlpool.seeds()],
)
)?;
Ok(())
}
fn remove_position_token_mint_authority<'info>(
whirlpool: &Account<'info, Whirlpool>,
position_mint: &Account<'info, Mint>,
token_program: &Program<'info, Token>,
) -> ProgramResult {
) -> Result<()> {
invoke_signed(
&set_authority(
token_program.key,
@ -210,5 +214,170 @@ fn remove_position_token_mint_authority<'info>(
token_program.to_account_info(),
],
&[&whirlpool.seeds()],
)?;
Ok(())
}
pub fn mint_position_bundle_token_and_remove_authority<'info>(
position_bundle: &Account<'info, PositionBundle>,
position_bundle_mint: &Account<'info, Mint>,
position_bundle_token_account: &Account<'info, TokenAccount>,
token_program: &Program<'info, Token>,
position_bundle_seeds: &[&[u8]],
) -> Result<()> {
mint_position_bundle_token(
position_bundle,
position_bundle_mint,
position_bundle_token_account,
token_program,
position_bundle_seeds,
)?;
remove_position_bundle_token_mint_authority(
position_bundle,
position_bundle_mint,
token_program,
position_bundle_seeds,
)
}
pub fn mint_position_bundle_token_with_metadata_and_remove_authority<'info>(
funder: &Signer<'info>,
position_bundle: &Account<'info, PositionBundle>,
position_bundle_mint: &Account<'info, Mint>,
position_bundle_token_account: &Account<'info, TokenAccount>,
position_bundle_metadata: &UncheckedAccount<'info>,
metadata_update_auth: &UncheckedAccount<'info>,
metadata_program: &UncheckedAccount<'info>,
token_program: &Program<'info, Token>,
system_program: &Program<'info, System>,
rent: &Sysvar<'info, Rent>,
position_bundle_seeds: &[&[u8]],
) -> Result<()> {
mint_position_bundle_token(
position_bundle,
position_bundle_mint,
position_bundle_token_account,
token_program,
position_bundle_seeds,
)?;
// Create Metadata
// Orca Position Bundle xxxx...yyyy
// xxxx and yyyy are the first and last 4 chars of mint address
let mint_address = position_bundle_mint.key().to_string();
let mut nft_name = String::from(WPB_METADATA_NAME_PREFIX);
nft_name += " ";
nft_name += &mint_address[0..4];
nft_name += "...";
nft_name += &mint_address[mint_address.len() - 4..];
invoke_signed(
&create_metadata_accounts_v3(
metadata_program.key(),
position_bundle_metadata.key(),
position_bundle_mint.key(),
position_bundle.key(),
funder.key(),
metadata_update_auth.key(),
nft_name,
WPB_METADATA_SYMBOL.to_string(),
WPB_METADATA_URI.to_string(),
None,
0,
false,
true,
None,
None,
None,
),
&[
position_bundle.to_account_info(),
position_bundle_metadata.to_account_info(),
position_bundle_mint.to_account_info(),
metadata_update_auth.to_account_info(),
funder.to_account_info(),
metadata_program.to_account_info(),
system_program.to_account_info(),
rent.to_account_info(),
],
&[position_bundle_seeds],
)?;
remove_position_bundle_token_mint_authority(
position_bundle,
position_bundle_mint,
token_program,
position_bundle_seeds,
)
}
fn mint_position_bundle_token<'info>(
position_bundle: &Account<'info, PositionBundle>,
position_bundle_mint: &Account<'info, Mint>,
position_bundle_token_account: &Account<'info, TokenAccount>,
token_program: &Program<'info, Token>,
position_bundle_seeds: &[&[u8]],
) -> Result<()> {
invoke_signed(
&mint_to(
token_program.key,
position_bundle_mint.to_account_info().key,
position_bundle_token_account.to_account_info().key,
position_bundle.to_account_info().key,
&[],
1,
)?,
&[
position_bundle_mint.to_account_info(),
position_bundle_token_account.to_account_info(),
position_bundle.to_account_info(),
token_program.to_account_info(),
],
&[position_bundle_seeds],
)?;
Ok(())
}
fn remove_position_bundle_token_mint_authority<'info>(
position_bundle: &Account<'info, PositionBundle>,
position_bundle_mint: &Account<'info, Mint>,
token_program: &Program<'info, Token>,
position_bundle_seeds: &[&[u8]],
) -> Result<()> {
invoke_signed(
&set_authority(
token_program.key,
position_bundle_mint.to_account_info().key,
Option::None,
AuthorityType::MintTokens,
position_bundle.to_account_info().key,
&[],
)?,
&[
position_bundle_mint.to_account_info(),
position_bundle.to_account_info(),
token_program.to_account_info(),
],
&[position_bundle_seeds],
)?;
Ok(())
}
pub fn burn_and_close_position_bundle_token<'info>(
position_bundle_authority: &Signer<'info>,
receiver: &UncheckedAccount<'info>,
position_bundle_mint: &Account<'info, Mint>,
position_bundle_token_account: &Account<'info, TokenAccount>,
token_program: &Program<'info, Token>,
) -> Result<()> {
// use same logic
burn_and_close_user_position_token(
position_bundle_authority,
receiver,
position_bundle_mint,
position_bundle_token_account,
token_program,
)
}

View File

@ -1,5 +1,5 @@
use anchor_lang::{
prelude::{AccountInfo, ProgramError, Pubkey, Signer},
prelude::{AccountInfo, Pubkey, Signer, *},
ToAccountInfo,
};
use anchor_spl::token::TokenAccount;
@ -8,10 +8,18 @@ use std::convert::TryFrom;
use crate::errors::ErrorCode;
pub fn verify_position_bundle_authority<'info>(
position_bundle_token_account: &TokenAccount,
position_bundle_authority: &Signer<'info>,
) -> Result<()> {
// use same logic
verify_position_authority(position_bundle_token_account, position_bundle_authority)
}
pub fn verify_position_authority<'info>(
position_token_account: &TokenAccount,
position_authority: &Signer<'info>,
) -> Result<(), ProgramError> {
) -> Result<()> {
// Check token authority using validate_owner method...
match position_token_account.delegate {
COption::Some(ref delegate) if position_authority.key == delegate => {
@ -28,10 +36,7 @@ pub fn verify_position_authority<'info>(
Ok(())
}
fn validate_owner(
expected_owner: &Pubkey,
owner_account_info: &AccountInfo,
) -> Result<(), ProgramError> {
fn validate_owner(expected_owner: &Pubkey, owner_account_info: &AccountInfo) -> Result<()> {
if expected_owner != owner_account_info.key || !owner_account_info.is_signer {
return Err(ErrorCode::MissingOrInvalidDelegate.into());
}
@ -39,6 +44,6 @@ fn validate_owner(
Ok(())
}
pub fn to_timestamp_u64(t: i64) -> Result<u64, ErrorCode> {
u64::try_from(t).or(Err(ErrorCode::InvalidTimestampConversion))
pub fn to_timestamp_u64(t: i64) -> Result<u64> {
u64::try_from(t).or(Err(ErrorCode::InvalidTimestampConversion.into()))
}

View File

@ -1,9 +1,18 @@
{
"version": "0.1.0",
"version": "0.2.0",
"name": "whirlpool",
"instructions": [
{
"name": "initializeConfig",
"docs": [
"Initializes a WhirlpoolsConfig account that hosts info & authorities",
"required to govern a set of Whirlpools.",
"",
"### 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."
],
"accounts": [
{
"name": "config",
@ -42,6 +51,20 @@
},
{
"name": "initializePool",
"docs": [
"Initializes a Whirlpool account.",
"Fee rate is set to the default values on the config and supplied fee_tier.",
"",
"### 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",
"`InvalidTokenMintOrder` - The order of mints have to be ordered by",
"`SqrtPriceOutOfBounds` - provided initial_sqrt_price is not between 2^-64 to 2^64",
""
],
"accounts": [
{
"name": "whirlpoolsConfig",
@ -118,6 +141,17 @@
},
{
"name": "initializeTickArray",
"docs": [
"Initializes a tick_array account to represent a tick-range in a Whirlpool.",
"",
"### 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",
"- `InvalidStartTick` - if the provided start tick is out of bounds or is not a multiple of",
"TICK_ARRAY_SIZE * tick spacing."
],
"accounts": [
{
"name": "whirlpool",
@ -149,6 +183,20 @@
},
{
"name": "initializeFeeTier",
"docs": [
"Initializes a fee_tier account usable by Whirlpools in a WhirlpoolConfig space.",
"",
"### Authority",
"- \"fee_authority\" - Set authority in the WhirlpoolConfig",
"",
"### 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",
"- `FeeRateMaxExceeded` - If the provided default_fee_rate exceeds MAX_FEE_RATE."
],
"accounts": [
{
"name": "config",
@ -189,6 +237,21 @@
},
{
"name": "initializeReward",
"docs": [
"Initialize reward for a Whirlpool. A pool can only support up to a set number of rewards.",
"",
"### Authority",
"- \"reward_authority\" - assigned authority by the reward_super_authority for the specified",
"reward-index in this Whirlpool",
"",
"### Parameters",
"- `reward_index` - The reward index that we'd like to initialize. (0 <= index <= NUM_REWARDS)",
"",
"#### 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."
],
"accounts": [
{
"name": "rewardAuthority",
@ -240,6 +303,25 @@
},
{
"name": "setRewardEmissions",
"docs": [
"Set the reward emissions for a reward in a Whirlpool.",
"",
"### Authority",
"- \"reward_authority\" - assigned authority by the reward_super_authority for the specified",
"reward-index in this Whirlpool",
"",
"### 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",
"- `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, or",
"all reward slots for this pool has been initialized."
],
"accounts": [
{
"name": "whirlpool",
@ -270,6 +352,18 @@
},
{
"name": "openPosition",
"docs": [
"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",
"- `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",
"- `InvalidTickIndex` - If a provided tick is out of bounds, out of order or not a multiple of",
"the tick-spacing in this pool."
],
"accounts": [
{
"name": "funder",
@ -341,6 +435,19 @@
},
{
"name": "openPositionWithMetadata",
"docs": [
"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.",
"",
"### 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",
"- `InvalidTickIndex` - If a provided tick is out of bounds, out of order or not a multiple of",
"the tick-spacing in this pool."
],
"accounts": [
{
"name": "funder",
@ -365,7 +472,10 @@
{
"name": "positionMetadataAccount",
"isMut": true,
"isSigner": false
"isSigner": false,
"docs": [
"https://github.com/metaplex-foundation/metaplex-program-library/blob/master/token-metadata/program/src/utils.rs#L873"
]
},
{
"name": "positionTokenAccount",
@ -427,6 +537,22 @@
},
{
"name": "increaseLiquidity",
"docs": [
"Add liquidity to a position in the Whirlpool. This call also updates the position's accrued fees and rewards.",
"",
"### Authority",
"- `position_authority` - authority that owns the token corresponding to this desired position.",
"",
"### 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",
"- `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."
],
"accounts": [
{
"name": "whirlpool",
@ -501,6 +627,22 @@
},
{
"name": "decreaseLiquidity",
"docs": [
"Withdraw liquidity from a position in the Whirlpool. This call also updates the position's accrued fees and rewards.",
"",
"### Authority",
"- `position_authority` - authority that owns the token corresponding to this desired position.",
"",
"### 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",
"- `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."
],
"accounts": [
{
"name": "whirlpool",
@ -575,6 +717,13 @@
},
{
"name": "updateFeesAndRewards",
"docs": [
"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."
],
"accounts": [
{
"name": "whirlpool",
@ -601,6 +750,12 @@
},
{
"name": "collectFees",
"docs": [
"Collect fees accrued for this position.",
"",
"### Authority",
"- `position_authority` - authority that owns the token corresponding to this desired position."
],
"accounts": [
{
"name": "whirlpool",
@ -652,6 +807,12 @@
},
{
"name": "collectReward",
"docs": [
"Collect rewards accrued for this position.",
"",
"### Authority",
"- `position_authority` - authority that owns the token corresponding to this desired position."
],
"accounts": [
{
"name": "whirlpool",
@ -698,6 +859,12 @@
},
{
"name": "collectProtocolFees",
"docs": [
"Collect the protocol fees accrued in this Whirlpool",
"",
"### Authority",
"- `collect_protocol_fees_authority` - assigned authority in the WhirlpoolConfig that can collect protocol fees"
],
"accounts": [
{
"name": "whirlpoolsConfig",
@ -744,6 +911,29 @@
},
{
"name": "swap",
"docs": [
"Perform a swap in this Whirlpool",
"",
"### 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 amount_specified_is_input).",
"- `other_amount_threshold` - The maximum/minimum of input/output token to swap into (depending on amount_specified_is_input).",
"- `sqrt_price_limit` - The maximum/minimum price the swap will swap to.",
"- `amount_specified_is_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."
],
"accounts": [
{
"name": "tokenProgram",
@ -826,6 +1016,15 @@
},
{
"name": "closePosition",
"docs": [
"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."
],
"accounts": [
{
"name": "positionAuthority",
@ -862,6 +1061,20 @@
},
{
"name": "setDefaultFeeRate",
"docs": [
"Set the default_fee_rate for a FeeTier",
"Only the current fee authority has permission to invoke this instruction.",
"",
"### Authority",
"- \"fee_authority\" - Set authority in the WhirlpoolConfig",
"",
"### Parameters",
"- `default_fee_rate` - The default fee rate that a pool will use if the pool uses this",
"fee tier during initialization.",
"",
"#### Special Errors",
"- `FeeRateMaxExceeded` - If the provided default_fee_rate exceeds MAX_FEE_RATE."
],
"accounts": [
{
"name": "whirlpoolsConfig",
@ -888,6 +1101,20 @@
},
{
"name": "setDefaultProtocolFeeRate",
"docs": [
"Sets the default protocol fee rate for a WhirlpoolConfig",
"Protocol fee rate is represented as a basis point.",
"Only the current fee authority has permission to invoke this instruction.",
"",
"### Authority",
"- \"fee_authority\" - Set authority that can modify pool fees in the WhirlpoolConfig",
"",
"### Parameters",
"- `default_protocol_fee_rate` - Rate that is referenced during the initialization of a Whirlpool using this config.",
"",
"#### Special Errors",
"- `ProtocolFeeRateMaxExceeded` - If the provided default_protocol_fee_rate exceeds MAX_PROTOCOL_FEE_RATE."
],
"accounts": [
{
"name": "whirlpoolsConfig",
@ -909,6 +1136,20 @@
},
{
"name": "setFeeRate",
"docs": [
"Sets the fee rate for a Whirlpool.",
"Fee rate is represented as hundredths of a basis point.",
"Only the current fee authority has permission to invoke this instruction.",
"",
"### Authority",
"- \"fee_authority\" - Set authority that can modify pool fees in the WhirlpoolConfig",
"",
"### Parameters",
"- `fee_rate` - The rate that the pool will use to calculate fees going onwards.",
"",
"#### Special Errors",
"- `FeeRateMaxExceeded` - If the provided fee_rate exceeds MAX_FEE_RATE."
],
"accounts": [
{
"name": "whirlpoolsConfig",
@ -935,6 +1176,20 @@
},
{
"name": "setProtocolFeeRate",
"docs": [
"Sets the protocol fee rate for a Whirlpool.",
"Protocol fee rate is represented as a basis point.",
"Only the current fee authority has permission to invoke this instruction.",
"",
"### Authority",
"- \"fee_authority\" - Set authority that can modify pool fees in the WhirlpoolConfig",
"",
"### Parameters",
"- `protocol_fee_rate` - The rate that the pool will use to calculate protocol fees going onwards.",
"",
"#### Special Errors",
"- `ProtocolFeeRateMaxExceeded` - If the provided default_protocol_fee_rate exceeds MAX_PROTOCOL_FEE_RATE."
],
"accounts": [
{
"name": "whirlpoolsConfig",
@ -961,6 +1216,15 @@
},
{
"name": "setFeeAuthority",
"docs": [
"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 fee authority has permission to invoke this instruction.",
"",
"### Authority",
"- \"fee_authority\" - Set authority that can modify pool fees in the WhirlpoolConfig"
],
"accounts": [
{
"name": "whirlpoolsConfig",
@ -982,6 +1246,13 @@
},
{
"name": "setCollectProtocolFeesAuthority",
"docs": [
"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",
"- \"fee_authority\" - Set authority that can collect protocol fees in the WhirlpoolConfig"
],
"accounts": [
{
"name": "whirlpoolsConfig",
@ -1003,6 +1274,18 @@
},
{
"name": "setRewardAuthority",
"docs": [
"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",
"- \"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."
],
"accounts": [
{
"name": "whirlpool",
@ -1029,6 +1312,18 @@
},
{
"name": "setRewardAuthorityBySuperAuthority",
"docs": [
"Set the whirlpool reward authority at the provided `reward_index`.",
"Only the current reward super authority has permission to invoke this instruction.",
"",
"### 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."
],
"accounts": [
{
"name": "whirlpoolsConfig",
@ -1060,6 +1355,14 @@
},
{
"name": "setRewardEmissionsSuperAuthority",
"docs": [
"Set the whirlpool reward super authority for a WhirlpoolConfig",
"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",
"- \"reward_emissions_super_authority\" - Set authority that can control reward authorities for all pools in this config space."
],
"accounts": [
{
"name": "whirlpoolsConfig",
@ -1081,6 +1384,33 @@
},
{
"name": "twoHopSwap",
"docs": [
"Perform a two-hop swap in this Whirlpool",
"",
"### 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 amount_specified_is_input).",
"- `other_amount_threshold` - The maximum/minimum of input/output token to swap into (depending on amount_specified_is_input).",
"- `amount_specified_is_input` - Specifies the token the parameter `amount`represents. If true, the amount represents the input token of the swap.",
"- `a_to_b_one` - The direction of the swap of hop one. True if swapping from A to B. False if swapping from B to A.",
"- `a_to_b_two` - The direction of the swap of hop two. True if swapping from A to B. False if swapping from B to A.",
"- `sqrt_price_limit_one` - The maximum/minimum price the swap will swap to in the first hop.",
"- `sqrt_price_limit_two` - The maximum/minimum price the swap will swap to in the second hop.",
"",
"#### 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.",
"- `InvalidIntermediaryMint` - Error if the intermediary mint between hop one and two do not equal.",
"- `DuplicateTwoHopPool` - Error if whirlpool one & two are the same pool."
],
"accounts": [
{
"name": "tokenProgram",
@ -1213,6 +1543,306 @@
"type": "u128"
}
]
},
{
"name": "initializePositionBundle",
"docs": [
"Initializes a PositionBundle account that bundles several positions.",
"A unique token will be minted to represent the position bundle in the users wallet."
],
"accounts": [
{
"name": "positionBundle",
"isMut": true,
"isSigner": false
},
{
"name": "positionBundleMint",
"isMut": true,
"isSigner": true
},
{
"name": "positionBundleTokenAccount",
"isMut": true,
"isSigner": false
},
{
"name": "positionBundleOwner",
"isMut": false,
"isSigner": false
},
{
"name": "funder",
"isMut": true,
"isSigner": true
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
},
{
"name": "systemProgram",
"isMut": false,
"isSigner": false
},
{
"name": "rent",
"isMut": false,
"isSigner": false
},
{
"name": "associatedTokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": []
},
{
"name": "initializePositionBundleWithMetadata",
"docs": [
"Initializes a PositionBundle account that bundles several positions.",
"A unique token will be minted to represent the position bundle in the users wallet.",
"Additional Metaplex metadata is appended to identify the token."
],
"accounts": [
{
"name": "positionBundle",
"isMut": true,
"isSigner": false
},
{
"name": "positionBundleMint",
"isMut": true,
"isSigner": true
},
{
"name": "positionBundleMetadata",
"isMut": true,
"isSigner": false,
"docs": [
"https://github.com/metaplex-foundation/metaplex-program-library/blob/773a574c4b34e5b9f248a81306ec24db064e255f/token-metadata/program/src/utils/metadata.rs#L100"
]
},
{
"name": "positionBundleTokenAccount",
"isMut": true,
"isSigner": false
},
{
"name": "positionBundleOwner",
"isMut": false,
"isSigner": false
},
{
"name": "funder",
"isMut": true,
"isSigner": true
},
{
"name": "metadataUpdateAuth",
"isMut": false,
"isSigner": false
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
},
{
"name": "systemProgram",
"isMut": false,
"isSigner": false
},
{
"name": "rent",
"isMut": false,
"isSigner": false
},
{
"name": "associatedTokenProgram",
"isMut": false,
"isSigner": false
},
{
"name": "metadataProgram",
"isMut": false,
"isSigner": false
}
],
"args": []
},
{
"name": "deletePositionBundle",
"docs": [
"Delete a PositionBundle account. Burns the position bundle token in the owner's wallet.",
"",
"### Authority",
"- `position_bundle_owner` - The owner that owns the position bundle token.",
"",
"### Special Errors",
"- `PositionBundleNotDeletable` - The provided position bundle has open positions."
],
"accounts": [
{
"name": "positionBundle",
"isMut": true,
"isSigner": false
},
{
"name": "positionBundleMint",
"isMut": true,
"isSigner": false
},
{
"name": "positionBundleTokenAccount",
"isMut": true,
"isSigner": false
},
{
"name": "positionBundleOwner",
"isMut": false,
"isSigner": true
},
{
"name": "receiver",
"isMut": true,
"isSigner": false
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": []
},
{
"name": "openBundledPosition",
"docs": [
"Open a bundled position in a Whirlpool. No new tokens are issued",
"because the owner of the position bundle becomes the owner of the position.",
"The position will start off with 0 liquidity.",
"",
"### Authority",
"- `position_bundle_authority` - authority that owns the token corresponding to this desired position bundle.",
"",
"### Parameters",
"- `bundle_index` - The bundle index that we'd like to open.",
"- `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",
"- `InvalidBundleIndex` - If the provided bundle index is out of bounds.",
"- `InvalidTickIndex` - If a provided tick is out of bounds, out of order or not a multiple of",
"the tick-spacing in this pool."
],
"accounts": [
{
"name": "bundledPosition",
"isMut": true,
"isSigner": false
},
{
"name": "positionBundle",
"isMut": true,
"isSigner": false
},
{
"name": "positionBundleTokenAccount",
"isMut": false,
"isSigner": false
},
{
"name": "positionBundleAuthority",
"isMut": false,
"isSigner": true
},
{
"name": "whirlpool",
"isMut": false,
"isSigner": false
},
{
"name": "funder",
"isMut": true,
"isSigner": true
},
{
"name": "systemProgram",
"isMut": false,
"isSigner": false
},
{
"name": "rent",
"isMut": false,
"isSigner": false
}
],
"args": [
{
"name": "bundleIndex",
"type": "u16"
},
{
"name": "tickLowerIndex",
"type": "i32"
},
{
"name": "tickUpperIndex",
"type": "i32"
}
]
},
{
"name": "closeBundledPosition",
"docs": [
"Close a bundled position in a Whirlpool.",
"",
"### Authority",
"- `position_bundle_authority` - authority that owns the token corresponding to this desired position bundle.",
"",
"### Parameters",
"- `bundle_index` - The bundle index that we'd like to close.",
"",
"#### Special Errors",
"- `InvalidBundleIndex` - If the provided bundle index is out of bounds.",
"- `ClosePositionNotEmpty` - The provided position account is not empty."
],
"accounts": [
{
"name": "bundledPosition",
"isMut": true,
"isSigner": false
},
{
"name": "positionBundle",
"isMut": true,
"isSigner": false
},
{
"name": "positionBundleTokenAccount",
"isMut": false,
"isSigner": false
},
{
"name": "positionBundleAuthority",
"isMut": false,
"isSigner": true
},
{
"name": "receiver",
"isMut": true,
"isSigner": false
}
],
"args": [
{
"name": "bundleIndex",
"type": "u16"
}
]
}
],
"accounts": [
@ -1260,6 +1890,27 @@
]
}
},
{
"name": "PositionBundle",
"type": {
"kind": "struct",
"fields": [
{
"name": "positionBundleMint",
"type": "publicKey"
},
{
"name": "positionBitmap",
"type": {
"array": [
"u8",
32
]
}
}
]
}
},
{
"name": "Position",
"type": {
@ -1528,27 +2179,49 @@
},
{
"name": "WhirlpoolRewardInfo",
"docs": [
"Stores the state relevant for tracking liquidity mining rewards at the `Whirlpool` level.",
"These values are used in conjunction with `PositionRewardInfo`, `Tick.reward_growths_outside`,",
"and `Whirlpool.reward_last_updated_timestamp` to determine how many rewards are earned by open",
"positions."
],
"type": {
"kind": "struct",
"fields": [
{
"name": "mint",
"docs": [
"Reward token mint."
],
"type": "publicKey"
},
{
"name": "vault",
"docs": [
"Reward vault token account."
],
"type": "publicKey"
},
{
"name": "authority",
"docs": [
"Authority account that has permission to initialize the reward and set emissions."
],
"type": "publicKey"
},
{
"name": "emissionsPerSecondX64",
"docs": [
"Q64.64 number that indicates how many tokens per second are earned per unit of liquidity."
],
"type": "u128"
},
{
"name": "growthGlobalX64",
"docs": [
"Q64.64 number that tracks the total tokens earned per unit of liquidity since the reward",
"emissions were turned on."
],
"type": "u128"
}
]
@ -1827,6 +2500,26 @@
"code": 6042,
"name": "DuplicateTwoHopPool",
"msg": "Duplicate two hop pool"
},
{
"code": 6043,
"name": "InvalidBundleIndex",
"msg": "Bundle index is out of bounds"
},
{
"code": 6044,
"name": "BundledPositionAlreadyOpened",
"msg": "Position has already been opened"
},
{
"code": 6045,
"name": "BundledPositionAlreadyClosed",
"msg": "Position has already been closed"
},
{
"code": 6046,
"name": "PositionBundleNotDeletable",
"msg": "Unable to delete PositionBundle with open positions"
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,66 @@
import { Instruction } from "@orca-so/common-sdk";
import { PublicKey } from "@solana/web3.js";
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
/**
* Parameters to close a bundled position in a Whirlpool.
*
* @category Instruction Types
* @param bundledPosition - PublicKey for the bundled position.
* @param positionBundle - PublicKey for the position bundle.
* @param positionBundleTokenAccount - The associated token address for the position bundle token in the owners wallet.
* @param positionBundleAuthority - authority that owns the token corresponding to this desired bundled position.
* @param bundleIndex - The bundle index that holds the bundled position.
* @param receiver - PublicKey for the wallet that will receive the rented lamports.
*/
export type CloseBundledPositionParams = {
bundledPosition: PublicKey;
positionBundle: PublicKey;
positionBundleTokenAccount: PublicKey;
positionBundleAuthority: PublicKey;
bundleIndex: number;
receiver: PublicKey;
};
/**
* Close a bundled position in a Whirlpool.
*
* #### Special Errors
* `InvalidBundleIndex` - If the provided bundle index is out of bounds.
* `ClosePositionNotEmpty` - The provided position account is not empty.
*
* @category Instructions
* @param program - program object containing services required to generate the instruction
* @param params - CloseBundledPositionParams object
* @returns - Instruction to perform the action.
*/
export function closeBundledPositionIx(
program: Program<Whirlpool>,
params: CloseBundledPositionParams
): Instruction {
const {
bundledPosition,
positionBundle,
positionBundleTokenAccount,
positionBundleAuthority,
bundleIndex,
receiver,
} = params;
const ix = program.instruction.closeBundledPosition(bundleIndex, {
accounts: {
bundledPosition,
positionBundle,
positionBundleTokenAccount,
positionBundleAuthority,
receiver,
},
});
return {
instructions: [ix],
cleanupInstructions: [],
signers: [],
};
}

View File

@ -0,0 +1,64 @@
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 { TOKEN_PROGRAM_ID } from "@solana/spl-token";
/**
* Parameters to delete a PositionBundle account.
*
* @category Instruction Types
* @param owner - PublicKey for the wallet that owns the position bundle token.
* @param positionBundle - PublicKey for the position bundle.
* @param positionBundleMint - PublicKey for the mint for the position bundle token.
* @param positionBundleTokenAccount - The associated token address for the position bundle token in the owners wallet.
* @param receiver - PublicKey for the wallet that will receive the rented lamports.
*/
export type DeletePositionBundleParams = {
owner: PublicKey;
positionBundle: PublicKey;
positionBundleMint: PublicKey;
positionBundleTokenAccount: PublicKey;
receiver: PublicKey;
};
/**
* Deletes a PositionBundle account.
*
* #### Special Errors
* `PositionBundleNotDeletable` - The provided position bundle has open positions.
*
* @category Instructions
* @param program - program object containing services required to generate the instruction
* @param params - DeletePositionBundleParams object
* @returns - Instruction to perform the action.
*/
export function deletePositionBundleIx(
program: Program<Whirlpool>,
params: DeletePositionBundleParams
): Instruction {
const {
owner,
positionBundle,
positionBundleMint,
positionBundleTokenAccount,
receiver,
} = params;
const ix = program.instruction.deletePositionBundle({
accounts: {
positionBundle: positionBundle,
positionBundleMint: positionBundleMint,
positionBundleTokenAccount,
positionBundleOwner: owner,
receiver,
tokenProgram: TOKEN_PROGRAM_ID,
},
});
return {
instructions: [ix],
cleanupInstructions: [],
signers: [],
};
}

View File

@ -1,15 +1,19 @@
export * from "./close-bundled-position-ix";
export * from "./close-position-ix";
export * from "./collect-fees-ix";
export * from "./collect-protocol-fees-ix";
export * from "./collect-reward-ix";
export * from "./composites";
export * from "./decrease-liquidity-ix";
export * from "./delete-position-bundle-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-position-bundle-ix";
export * from "./initialize-reward-ix";
export * from "./initialize-tick-array-ix";
export * from "./open-bundled-position-ix";
export * from "./open-position-ix";
export * from "./set-collect-protocol-fees-authority-ix";
export * from "./set-default-fee-rate-ix";

View File

@ -0,0 +1,113 @@
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, SystemProgram, Keypair } from "@solana/web3.js";
import { PDA } from "@orca-so/common-sdk";
import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { METADATA_PROGRAM_ADDRESS, WHIRLPOOL_NFT_UPDATE_AUTH } from "..";
/**
* Parameters to initialize a PositionBundle account.
*
* @category Instruction Types
* @param owner - PublicKey for the wallet that will host the minted position bundle token.
* @param positionBundlePda - PDA for the derived position bundle address.
* @param positionBundleMintKeypair - Keypair for the mint for the position bundle token.
* @param positionBundleTokenAccount - The associated token address for the position bundle token in the owners wallet.
* @param funder - The account that would fund the creation of this account
*/
export type InitializePositionBundleParams = {
owner: PublicKey;
positionBundlePda: PDA;
positionBundleMintKeypair: Keypair;
positionBundleTokenAccount: PublicKey;
funder: PublicKey;
};
/**
* Initializes a PositionBundle account.
*
* @category Instructions
* @param program - program object containing services required to generate the instruction
* @param params - InitializePositionBundleParams object
* @returns - Instruction to perform the action.
*/
export function initializePositionBundleIx(
program: Program<Whirlpool>,
params: InitializePositionBundleParams
): Instruction {
const {
owner,
positionBundlePda,
positionBundleMintKeypair,
positionBundleTokenAccount,
funder,
} = params;
const ix = program.instruction.initializePositionBundle({
accounts: {
positionBundle: positionBundlePda.publicKey,
positionBundleMint: positionBundleMintKeypair.publicKey,
positionBundleTokenAccount,
positionBundleOwner: owner,
funder,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
},
});
return {
instructions: [ix],
cleanupInstructions: [],
signers: [positionBundleMintKeypair],
};
}
/**
* Initializes a PositionBundle account.
* Additional Metaplex metadata is appended to identify the token.
*
* @category Instructions
* @param program - program object containing services required to generate the instruction
* @param params - InitializePositionBundleParams object
* @returns - Instruction to perform the action.
*/
export function initializePositionBundleWithMetadataIx(
program: Program<Whirlpool>,
params: InitializePositionBundleParams & { positionBundleMetadataPda: PDA }
): Instruction {
const {
owner,
positionBundlePda,
positionBundleMintKeypair,
positionBundleTokenAccount,
positionBundleMetadataPda,
funder,
} = params;
const ix = program.instruction.initializePositionBundleWithMetadata({
accounts: {
positionBundle: positionBundlePda.publicKey,
positionBundleMint: positionBundleMintKeypair.publicKey,
positionBundleMetadata: positionBundleMetadataPda.publicKey,
positionBundleTokenAccount,
positionBundleOwner: owner,
funder,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
metadataProgram: METADATA_PROGRAM_ADDRESS,
metadataUpdateAuth: WHIRLPOOL_NFT_UPDATE_AUTH,
},
});
return {
instructions: [ix],
cleanupInstructions: [],
signers: [positionBundleMintKeypair],
};
}

View File

@ -0,0 +1,81 @@
import { Program } from "@project-serum/anchor";
import { Whirlpool } from "../artifacts/whirlpool";
import { PublicKey, SystemProgram } from "@solana/web3.js";
import { PDA, Instruction } from "@orca-so/common-sdk";
import * as anchor from "@project-serum/anchor";
/**
* Parameters to open a bundled position in a Whirlpool.
*
* @category Instruction Types
* @param whirlpool - PublicKey for the whirlpool that the bundled position will be opened for.
* @param bundledPositionPda - PDA for the derived bundled position address.
* @param positionBundle - PublicKey for the position bundle.
* @param positionBundleTokenAccount - The associated token address for the position bundle token in the owners wallet.
* @param positionBundleAuthority - authority that owns the token corresponding to this desired bundled position.
* @param bundleIndex - The bundle index that holds the bundled position.
* @param tickLowerIndex - The tick specifying the lower end of the bundled position range.
* @param tickUpperIndex - The tick specifying the upper end of the bundled position range.
* @param funder - The account that would fund the creation of this account
*/
export type OpenBundledPositionParams = {
whirlpool: PublicKey;
bundledPositionPda: PDA;
positionBundle: PublicKey;
positionBundleTokenAccount: PublicKey;
positionBundleAuthority: PublicKey;
bundleIndex: number;
tickLowerIndex: number;
tickUpperIndex: number;
funder: PublicKey;
};
/**
* Open a bundled position in a Whirlpool.
* No new tokens are issued because the owner of the position bundle becomes the owner of the position.
* The position will start off with 0 liquidity.
*
* #### Special Errors
* `InvalidBundleIndex` - If the provided bundle index is out of bounds.
* `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 program - program object containing services required to generate the instruction
* @param params - OpenBundledPositionParams object
* @returns - Instruction to perform the action.
*/
export function openBundledPositionIx(
program: Program<Whirlpool>,
params: OpenBundledPositionParams
): Instruction {
const {
whirlpool,
bundledPositionPda,
positionBundle,
positionBundleTokenAccount,
positionBundleAuthority,
bundleIndex,
tickLowerIndex,
tickUpperIndex,
funder,
} = params;
const ix = program.instruction.openBundledPosition(bundleIndex, tickLowerIndex, tickUpperIndex, {
accounts: {
bundledPosition: bundledPositionPda.publicKey,
positionBundle,
positionBundleTokenAccount,
positionBundleAuthority,
whirlpool,
funder,
systemProgram: SystemProgram.programId,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
},
});
return {
instructions: [ix],
cleanupInstructions: [],
signers: [],
};
}

View File

@ -2,7 +2,7 @@ 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 { METADATA_PROGRAM_ADDRESS, WHIRLPOOL_NFT_UPDATE_AUTH } from "..";
import {
OpenPositionBumpsData,
OpenPositionWithMetadataBumpsData,
@ -96,7 +96,7 @@ export function openPositionWithMetadataIx(
...openPositionAccounts(params),
positionMetadataAccount: metadataPda.publicKey,
metadataProgram: METADATA_PROGRAM_ADDRESS,
metadataUpdateAuth: new PublicKey("3axbTs2z5GBy6usVbNVoqEgZMng3vZvMnAoX29BFfwhr"),
metadataUpdateAuth: WHIRLPOOL_NFT_UPDATE_AUTH,
},
});

View File

@ -88,7 +88,6 @@ export class WhirlpoolIx {
* #### 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.
@ -105,7 +104,6 @@ export class WhirlpoolIx {
* #### 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.
@ -158,7 +156,6 @@ export class WhirlpoolIx {
/**
* 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.
@ -261,7 +258,6 @@ export class WhirlpoolIx {
* 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.
@ -441,6 +437,90 @@ export class WhirlpoolIx {
return ix.setRewardEmissionsSuperAuthorityIx(program, params);
}
/**
* Initializes a PositionBundle account.
*
* @param program - program object containing services required to generate the instruction
* @param params - InitializePositionBundleParams object
* @returns - Instruction to perform the action.
*/
public static initializePositionBundleIx(
program: Program<Whirlpool>,
params: ix.InitializePositionBundleParams
) {
return ix.initializePositionBundleIx(program, params);
}
/**
* Initializes a PositionBundle account.
* Additional Metaplex metadata is appended to identify the token.
*
* @param program - program object containing services required to generate the instruction
* @param params - InitializePositionBundleParams object
* @returns - Instruction to perform the action.
*/
public static initializePositionBundleWithMetadataIx(
program: Program<Whirlpool>,
params: ix.InitializePositionBundleParams & { positionBundleMetadataPda: PDA }
) {
return ix.initializePositionBundleWithMetadataIx(program, params);
}
/**
* Deletes a PositionBundle account.
*
* #### Special Errors
* `PositionBundleNotDeletable` - The provided position bundle has open positions.
*
* @param program - program object containing services required to generate the instruction
* @param params - DeletePositionBundleParams object
* @returns - Instruction to perform the action.
*/
public static deletePositionBundleIx(
program: Program<Whirlpool>,
params: ix.DeletePositionBundleParams
) {
return ix.deletePositionBundleIx(program, params);
}
/**
* Open a bundled position in a Whirlpool.
* No new tokens are issued because the owner of the position bundle becomes the owner of the position.
* The position will start off with 0 liquidity.
*
* #### Special Errors
* `InvalidBundleIndex` - If the provided bundle index is out of bounds.
* `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 - OpenBundledPositionParams object
* @returns - Instruction to perform the action.
*/
public static openBundledPositionIx(
program: Program<Whirlpool>,
params: ix.OpenBundledPositionParams
) {
return ix.openBundledPositionIx(program, params);
}
/**
* Close a bundled position in a Whirlpool.
*
* #### Special Errors
* `InvalidBundleIndex` - If the provided bundle index is out of bounds.
* `ClosePositionNotEmpty` - The provided position account is not empty.
*
* @param program - program object containing services required to generate the instruction
* @param params - CloseBundledPositionParams object
* @returns - Instruction to perform the action.
*/
public static closeBundledPositionIx(
program: Program<Whirlpool>,
params: ix.CloseBundledPositionParams
) {
return ix.closeBundledPositionIx(program, params);
}
/**
* DEPRECATED - use ${@link WhirlpoolClient} collectFeesAndRewardsForPositions function
* A set of transactions to collect all fees and rewards from a list of positions.

View File

@ -5,6 +5,7 @@ import { Connection, PublicKey } from "@solana/web3.js";
import invariant from "tiny-invariant";
import {
AccountName,
PositionBundleData,
PositionData,
TickArrayData,
WhirlpoolData,
@ -18,6 +19,7 @@ import {
ParsableFeeTier,
ParsableMintInfo,
ParsablePosition,
ParsablePositionBundle,
ParsableTickArray,
ParsableTokenInfo,
ParsableWhirlpool,
@ -33,6 +35,7 @@ type CachedValue =
| PositionData
| TickArrayData
| FeeTierData
| PositionBundleData
| AccountInfo
| MintInfo;
@ -181,6 +184,17 @@ export class AccountFetcher {
return this.get(AddressUtil.toPubKey(address), ParsableWhirlpoolsConfig, refresh);
}
/**
* Retrieve a cached position bundle account. Fetch from rpc on cache miss.
*
* @param address position bundle address
* @param refresh force cache refresh
* @returns position bundle account
*/
public async getPositionBundle(address: Address, refresh = false): Promise<PositionBundleData | null> {
return this.get(AddressUtil.toPubKey(address), ParsablePositionBundle, refresh);
}
/**
* Retrieve a list of cached whirlpool accounts. Fetch from rpc for cache misses.
*
@ -284,6 +298,20 @@ export class AccountFetcher {
return this.list(AddressUtil.toPubKeys(addresses), ParsableMintInfo, refresh);
}
/**
* Retrieve a list of cached position bundle accounts. Fetch from rpc for cache misses.
*
* @param addresses position bundle addresses
* @param refresh force cache refresh
* @returns position bundle accounts
*/
public async listPositionBundles(
addresses: Address[],
refresh: boolean
): Promise<(PositionBundleData | null)[]> {
return this.list(AddressUtil.toPubKeys(addresses), ParsablePositionBundle, refresh);
}
/**
* Update the cached value of all entities currently in the cache.
* Uses batched rpc request for network efficient fetch.

View File

@ -7,6 +7,7 @@ import {
TickArrayData,
AccountName,
FeeTierData,
PositionBundleData,
} from "../../types/public";
import { BorshAccountsCoder, Idl } from "@project-serum/anchor";
import * as WhirlpoolIDL from "../../artifacts/whirlpool.json";
@ -131,6 +132,27 @@ export class ParsableFeeTier {
}
}
/**
* @category Parsables
*/
@staticImplements<ParsableEntity<PositionBundleData>>()
export class ParsablePositionBundle {
private constructor() {}
public static parse(data: Buffer | undefined | null): PositionBundleData | null {
if (!data) {
return null;
}
try {
return parseAnchorAccount(AccountName.PositionBundle, data);
} catch (e) {
console.error(`error while parsing PositionBundle: ${e}`);
return null;
}
}
}
/**
* @category Parsables
*/
@ -173,7 +195,7 @@ export class ParsableMintInfo {
decimals: buffer.decimals,
isInitialized: buffer.isInitialized !== 0,
freezeAuthority:
buffer.freezeAuthority === 0 ? null : new PublicKey(buffer.freezeAuthority),
buffer.freezeAuthorityOption === 0 ? null : new PublicKey(buffer.freezeAuthority),
};
return mintInfo;

View File

@ -20,6 +20,7 @@ export enum AccountName {
TickArray = "TickArray",
Whirlpool = "Whirlpool",
FeeTier = "FeeTier",
PositionBundle = "PositionBundle",
}
const IDL = WhirlpoolIDL as Idl;
@ -157,3 +158,11 @@ export type FeeTierData = {
tickSpacing: number;
defaultFeeRate: number;
};
/**
* @category Solana Accounts
*/
export type PositionBundleData = {
positionBundleMint: PublicKey;
positionBitmap: number[];
};

View File

@ -57,6 +57,12 @@ export const MIN_SQRT_PRICE = "4295048016";
*/
export const TICK_ARRAY_SIZE = 88;
/**
* The number of bundled positions that a position-bundle account can hold.
* @category Constants
*/
export const POSITION_BUNDLE_SIZE = 256;
/**
* @category Constants
*/
@ -81,3 +87,11 @@ export const PROTOCOL_FEE_RATE_MUL_VALUE = new BN(10_000);
* @category Constants
*/
export const FEE_RATE_MUL_VALUE = new BN(1_000_000);
/**
* The public key that is allowed to update the metadata of Whirlpool NFTs.
* @category Constants
*/
export const WHIRLPOOL_NFT_UPDATE_AUTH = new PublicKey(
"3axbTs2z5GBy6usVbNVoqEgZMng3vZvMnAoX29BFfwhr"
);

View File

@ -27,6 +27,10 @@ export {
SwapInput,
SwapParams,
UpdateFeesAndRewardsParams,
InitializePositionBundleParams,
DeletePositionBundleParams,
OpenBundledPositionParams,
CloseBundledPositionParams,
} from "../../instructions/";
export {
CollectAllParams,

View File

@ -2,6 +2,7 @@ export * from "../graphs/public";
export * from "./ix-utils";
export * from "./pda-utils";
export * from "./pool-utils";
export * from "./position-bundle-util";
export * from "./price-math";
export * from "./swap-utils";
export * from "./tick-utils";

View File

@ -11,6 +11,8 @@ const PDA_METADATA_SEED = "metadata";
const PDA_TICK_ARRAY_SEED = "tick_array";
const PDA_FEE_TIER_SEED = "fee_tier";
const PDA_ORACLE_SEED = "oracle";
const PDA_POSITION_BUNDLE_SEED = "position_bundle";
const PDA_BUNDLED_POSITION_SEED = "bundled_position";
/**
* @category Whirlpool Utils
@ -168,4 +170,61 @@ export class PDAUtil {
programId
);
}
/**
* @category Program Derived Addresses
* @param programId
* @param positionBundleMintKey
* @param bundleIndex
* @returns
*/
public static getBundledPosition(
programId: PublicKey,
positionBundleMintKey: PublicKey,
bundleIndex: number
) {
return AddressUtil.findProgramAddress(
[
Buffer.from(PDA_BUNDLED_POSITION_SEED),
positionBundleMintKey.toBuffer(),
Buffer.from(bundleIndex.toString()),
],
programId
);
}
/**
* @category Program Derived Addresses
* @param programId
* @param positionBundleMintKey
* @returns
*/
public static getPositionBundle(
programId: PublicKey,
positionBundleMintKey: PublicKey,
) {
return AddressUtil.findProgramAddress(
[
Buffer.from(PDA_POSITION_BUNDLE_SEED),
positionBundleMintKey.toBuffer(),
],
programId
);
}
/**
* @category Program Derived Addresses
* @param positionBundleMintKey
* @returns
*/
public static getPositionBundleMetadata(positionBundleMintKey: PublicKey) {
return AddressUtil.findProgramAddress(
[
Buffer.from(PDA_METADATA_SEED),
METADATA_PROGRAM_ADDRESS.toBuffer(),
positionBundleMintKey.toBuffer(),
],
METADATA_PROGRAM_ADDRESS
);
}
}

View File

@ -0,0 +1,129 @@
import invariant from "tiny-invariant";
import {
PositionBundleData,
POSITION_BUNDLE_SIZE,
} from "../../types/public";
/**
* A collection of utility functions when interacting with a PositionBundle.
* @category Whirlpool Utils
*/
export class PositionBundleUtil {
private constructor() {}
/**
* Check if the bundle index is in the correct range.
*
* @param bundleIndex The bundle index to be checked
* @returns true if bundle index is in the correct range
*/
public static checkBundleIndexInBounds(bundleIndex: number): boolean {
return bundleIndex >= 0 && bundleIndex < POSITION_BUNDLE_SIZE;
}
/**
* Check if the Bundled Position corresponding to the bundle index has been opened.
*
* @param positionBundle The position bundle to be checked
* @param bundleIndex The bundle index to be checked
* @returns true if Bundled Position has been opened
*/
public static isOccupied(positionBundle: PositionBundleData, bundleIndex: number): boolean {
invariant(PositionBundleUtil.checkBundleIndexInBounds(bundleIndex), "bundleIndex out of range");
const array = PositionBundleUtil.convertBitmapToArray(positionBundle);
return array[bundleIndex];
}
/**
* Check if the Bundled Position corresponding to the bundle index has not been opened.
*
* @param positionBundle The position bundle to be checked
* @param bundleIndex The bundle index to be checked
* @returns true if Bundled Position has not been opened
*/
public static isUnoccupied(positionBundle: PositionBundleData, bundleIndex: number): boolean {
return !PositionBundleUtil.isOccupied(positionBundle, bundleIndex);
}
/**
* Check if all bundle index is occupied.
*
* @param positionBundle The position bundle to be checked
* @returns true if all bundle index is occupied
*/
public static isFull(positionBundle: PositionBundleData): boolean {
const unoccupied = PositionBundleUtil.getUnoccupiedBundleIndexes(positionBundle);
return unoccupied.length === 0;
}
/**
* Check if all bundle index is unoccupied.
*
* @param positionBundle The position bundle to be checked
* @returns true if all bundle index is unoccupied
*/
public static isEmpty(positionBundle: PositionBundleData): boolean {
const occupied = PositionBundleUtil.getOccupiedBundleIndexes(positionBundle);
return occupied.length === 0;
}
/**
* Get all bundle indexes where the corresponding Bundled Position is open.
*
* @param positionBundle The position bundle to be checked
* @returns The array of bundle index where the corresponding Bundled Position is open
*/
public static getOccupiedBundleIndexes(positionBundle: PositionBundleData): number[] {
const result: number[] = [];
PositionBundleUtil.convertBitmapToArray(positionBundle).forEach((occupied, index) => {
if (occupied) {
result.push(index);
}
})
return result;
}
/**
* Get all bundle indexes where the corresponding Bundled Position is not open.
*
* @param positionBundle The position bundle to be checked
* @returns The array of bundle index where the corresponding Bundled Position is not open
*/
public static getUnoccupiedBundleIndexes(positionBundle: PositionBundleData): number[] {
const result: number[] = [];
PositionBundleUtil.convertBitmapToArray(positionBundle).forEach((occupied, index) => {
if (!occupied) {
result.push(index);
}
})
return result;
}
/**
* Get the first unoccupied bundle index in the position bundle.
*
* @param positionBundle The position bundle to be checked
* @returns The first unoccupied bundle index, null if the position bundle is full
*/
public static findUnoccupiedBundleIndex(positionBundle: PositionBundleData): number|null {
const unoccupied = PositionBundleUtil.getUnoccupiedBundleIndexes(positionBundle);
return unoccupied.length === 0 ? null : unoccupied[0];
}
/**
* Convert position bitmap to the array of boolean which represent if Bundled Position is open.
*
* @param positionBundle The position bundle whose bitmap will be converted
* @returns The array of boolean representing if Bundled Position is open
*/
public static convertBitmapToArray(positionBundle: PositionBundleData): boolean[] {
const result: boolean[] = [];
positionBundle.positionBitmap.map((bitmap) => {
for (let offset=0; offset<8; offset++) {
result.push((bitmap & (1 << offset)) !== 0);
}
})
return result;
}
}

View File

@ -0,0 +1,642 @@
import * as anchor from "@project-serum/anchor";
import * as assert from "assert";
import {
buildWhirlpoolClient,
increaseLiquidityQuoteByInputTokenWithParams,
InitPoolParams,
PositionBundleData,
POSITION_BUNDLE_SIZE,
toTx,
WhirlpoolContext,
WhirlpoolIx,
} from "../../src";
import {
approveToken,
TickSpacing,
transfer,
ONE_SOL,
systemTransferTx,
createAssociatedTokenAccount,
} from "../utils";
import { PDA, Percentage } from "@orca-so/common-sdk";
import { initializePositionBundle, initTestPool, openBundledPosition, openPosition } from "../utils/init-utils";
import { u64 } from "@solana/spl-token";
import { mintTokensToTestAccount } from "../utils/test-builders";
describe("close_bundled_position", () => {
const provider = anchor.AnchorProvider.local(undefined, {
commitment: "confirmed",
preflightCommitment: "confirmed",
});
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Whirlpool;
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const client = buildWhirlpoolClient(ctx);
const fetcher = ctx.fetcher;
const tickLowerIndex = 0;
const tickUpperIndex = 128;
let poolInitInfo: InitPoolParams;
let whirlpoolPda: PDA;
const funderKeypair = anchor.web3.Keypair.generate();
before(async () => {
poolInitInfo = (await initTestPool(ctx, TickSpacing.Standard)).poolInitInfo;
whirlpoolPda = poolInitInfo.whirlpoolPda;
await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute();
const pool = await client.getPool(whirlpoolPda.publicKey);
await (await pool.initTickArrayForTicks([0]))?.buildAndExecute();
});
function checkBitmapIsOpened(account: PositionBundleData, bundleIndex: number): boolean {
if (bundleIndex < 0 || bundleIndex >= POSITION_BUNDLE_SIZE) throw Error("bundleIndex is out of bounds");
const bitmapIndex = Math.floor(bundleIndex / 8);
const bitmapOffset = bundleIndex % 8;
return (account.positionBitmap[bitmapIndex] & (1 << bitmapOffset)) > 0;
}
function checkBitmapIsClosed(account: PositionBundleData, bundleIndex: number): boolean {
if (bundleIndex < 0 || bundleIndex >= POSITION_BUNDLE_SIZE) throw Error("bundleIndex is out of bounds");
const bitmapIndex = Math.floor(bundleIndex / 8);
const bitmapOffset = bundleIndex % 8;
return (account.positionBitmap[bitmapIndex] & (1 << bitmapOffset)) === 0;
}
function checkBitmap(account: PositionBundleData, openedBundleIndexes: number[]) {
for (let i=0; i<POSITION_BUNDLE_SIZE; i++) {
if (openedBundleIndexes.includes(i)) {
assert.ok(checkBitmapIsOpened(account, i));
}
else {
assert.ok(checkBitmapIsClosed(account, i));
}
}
}
it("successfully closes an opened bundled position", async () => {
const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey);
const bundleIndex = 0;
const positionInitInfo = await openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex
);
const { bundledPositionPda } = positionInitInfo.params;
const preAccount = await fetcher.getPosition(bundledPositionPda.publicKey, true);
const prePositionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true);
checkBitmap(prePositionBundle!, [bundleIndex]);
assert.ok(preAccount !== null);
const receiverKeypair = anchor.web3.Keypair.generate();
await toTx(
ctx,
WhirlpoolIx.closeBundledPositionIx(ctx.program, {
bundledPosition: bundledPositionPda.publicKey,
bundleIndex,
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleAuthority: ctx.wallet.publicKey,
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount,
receiver: receiverKeypair.publicKey,
})
).buildAndExecute();
const postAccount = await fetcher.getPosition(bundledPositionPda.publicKey, true);
const postPositionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true);
checkBitmap(postPositionBundle!, []);
assert.ok(postAccount === null);
const receiverAccount = await provider.connection.getAccountInfo(receiverKeypair.publicKey);
const lamports = receiverAccount?.lamports;
assert.ok(lamports != undefined && lamports > 0);
});
it("should be failed: invalid bundle index", async () => {
const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey);
const bundleIndex = 0;
const positionInitInfo = await openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex
);
const { bundledPositionPda } = positionInitInfo.params;
const tx = await toTx(
ctx,
WhirlpoolIx.closeBundledPositionIx(ctx.program, {
bundledPosition: bundledPositionPda.publicKey,
bundleIndex: 1, // invalid
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleAuthority: ctx.wallet.publicKey,
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount,
receiver: ctx.wallet.publicKey,
})
);
await assert.rejects(
tx.buildAndExecute(),
/0x7d6/ // ConstraintSeeds (seed constraint was violated)
);
});
it("should be failed: user closes bundled position already closed", async () => {
const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey);
const bundleIndex = 0;
const positionInitInfo = await openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex
);
const { bundledPositionPda } = positionInitInfo.params;
const tx = toTx(
ctx,
WhirlpoolIx.closeBundledPositionIx(ctx.program, {
bundledPosition: bundledPositionPda.publicKey,
bundleIndex,
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleAuthority: ctx.wallet.publicKey,
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount,
receiver: ctx.wallet.publicKey,
})
);
// close...
await tx.buildAndExecute();
// re-close...
await assert.rejects(
tx.buildAndExecute(),
/0xbc4/ // AccountNotInitialized
);
});
it("should be failed: bundled position is not empty", async () => {
const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey);
const bundleIndex = 0;
const positionInitInfo = await openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex
);
const { bundledPositionPda } = positionInitInfo.params;
// deposit
const pool = await client.getPool(poolInitInfo.whirlpoolPda.publicKey, true);
const quote = increaseLiquidityQuoteByInputTokenWithParams({
tokenMintA: poolInitInfo.tokenMintA,
tokenMintB: poolInitInfo.tokenMintB,
sqrtPrice: pool.getData().sqrtPrice,
slippageTolerance: Percentage.fromFraction(0, 100),
tickLowerIndex,
tickUpperIndex,
tickCurrentIndex: pool.getData().tickCurrentIndex,
inputTokenMint: poolInitInfo.tokenMintB,
inputTokenAmount: new u64(1_000_000),
});
await mintTokensToTestAccount(
provider,
poolInitInfo.tokenMintA,
quote.tokenMaxA.toNumber(),
poolInitInfo.tokenMintB,
quote.tokenMaxB.toNumber(),
ctx.wallet.publicKey
);
const position = await client.getPosition(bundledPositionPda.publicKey, true);
await (await position.increaseLiquidity(quote)).buildAndExecute();
assert.ok((await position.refreshData()).liquidity.gtn(0));
// try to close...
const tx = toTx(
ctx,
WhirlpoolIx.closeBundledPositionIx(ctx.program, {
bundledPosition: bundledPositionPda.publicKey,
bundleIndex,
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleAuthority: ctx.wallet.publicKey,
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount,
receiver: ctx.wallet.publicKey,
})
);
await assert.rejects(
tx.buildAndExecute(),
/0x1775/ // ClosePositionNotEmpty
);
});
describe("invalid input account", () => {
it("should be failed: invalid bundled position", async () => {
const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey);
const positionInitInfo0 = await openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
0,
tickLowerIndex,
tickUpperIndex
);
const positionInitInfo1 = await openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
1,
tickLowerIndex,
tickUpperIndex
);
const tx = toTx(
ctx,
WhirlpoolIx.closeBundledPositionIx(ctx.program, {
bundledPosition: positionInitInfo1.params.bundledPositionPda.publicKey, // invalid
bundleIndex: 0,
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleAuthority: ctx.wallet.publicKey,
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount,
receiver: ctx.wallet.publicKey,
})
);
await assert.rejects(
tx.buildAndExecute(),
/0x7d6/ // ConstraintSeeds (seed constraint was violated)
);
});
it("should be failed: invalid position bundle", async () => {
const positionBundleInfo0 = await initializePositionBundle(ctx, ctx.wallet.publicKey);
const positionBundleInfo1 = await initializePositionBundle(ctx, ctx.wallet.publicKey);
const bundleIndex = 0;
const positionInitInfo = await openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo0.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex
);
const tx = toTx(
ctx,
WhirlpoolIx.closeBundledPositionIx(ctx.program, {
bundledPosition: positionInitInfo.params.bundledPositionPda.publicKey,
bundleIndex,
positionBundle: positionBundleInfo1.positionBundlePda.publicKey, // invalid
positionBundleAuthority: ctx.wallet.publicKey,
positionBundleTokenAccount: positionBundleInfo0.positionBundleTokenAccount,
receiver: ctx.wallet.publicKey,
})
);
await assert.rejects(
tx.buildAndExecute(),
/0x7d6/ // ConstraintSeeds (seed constraint was violated)
);
});
it("should be failed: invalid ATA (amount is zero)", async () => {
const positionBundleInfo = await initializePositionBundle(ctx);
const bundleIndex = 0;
const positionInitInfo = await openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex
);
const ata = await createAssociatedTokenAccount(
provider,
positionBundleInfo.positionBundleMintKeypair.publicKey,
funderKeypair.publicKey,
ctx.wallet.publicKey,
);
const tx = toTx(
ctx,
WhirlpoolIx.closeBundledPositionIx(ctx.program, {
bundledPosition: positionInitInfo.params.bundledPositionPda.publicKey,
bundleIndex,
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleAuthority: ctx.wallet.publicKey,
positionBundleTokenAccount: ata, // invalid
receiver: ctx.wallet.publicKey,
})
);
await assert.rejects(
tx.buildAndExecute(),
/0x7d3/ // ConstraintRaw (amount == 1)
);
});
it("should be failed: invalid ATA (invalid mint)", async () => {
const positionBundleInfo0 = await initializePositionBundle(ctx, ctx.wallet.publicKey);
const positionBundleInfo1 = await initializePositionBundle(ctx, ctx.wallet.publicKey);
const bundleIndex = 0;
const positionInitInfo = await openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo0.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex
);
const tx = toTx(
ctx,
WhirlpoolIx.closeBundledPositionIx(ctx.program, {
bundledPosition: positionInitInfo.params.bundledPositionPda.publicKey,
bundleIndex,
positionBundle: positionBundleInfo0.positionBundlePda.publicKey,
positionBundleAuthority: ctx.wallet.publicKey,
positionBundleTokenAccount: positionBundleInfo1.positionBundleTokenAccount, // invalid
receiver: ctx.wallet.publicKey,
})
);
await assert.rejects(
tx.buildAndExecute(),
/0x7d3/ // ConstraintRaw (mint == position_bundle.position_bundle_mint)
);
});
it("should be failed: invalid position bundle authority", async () => {
const positionBundleInfo = await initializePositionBundle(ctx);
const bundleIndex = 0;
const positionInitInfo = await openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex
);
const tx = toTx(
ctx,
WhirlpoolIx.closeBundledPositionIx(ctx.program, {
bundledPosition: positionInitInfo.params.bundledPositionPda.publicKey,
bundleIndex,
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleAuthority: funderKeypair.publicKey, // invalid
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount,
receiver: ctx.wallet.publicKey,
})
);
tx.addSigner(funderKeypair);
await assert.rejects(
tx.buildAndExecute(),
/0x1783/ // MissingOrInvalidDelegate
);
});
});
describe("authority delegation", () => {
it("successfully closes bundled position with delegated authority", async () => {
const positionBundleInfo = await initializePositionBundle(ctx);
const bundleIndex = 0;
const positionInitInfo = await openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex
);
const tx = toTx(
ctx,
WhirlpoolIx.closeBundledPositionIx(ctx.program, {
bundledPosition: positionInitInfo.params.bundledPositionPda.publicKey,
bundleIndex,
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleAuthority: funderKeypair.publicKey, // should be delegated
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount,
receiver: ctx.wallet.publicKey,
})
);
tx.addSigner(funderKeypair);
await assert.rejects(
tx.buildAndExecute(),
/0x1783/ // MissingOrInvalidDelegate
);
// delegate 1 token from ctx.wallet to funder
await approveToken(
provider,
positionBundleInfo.positionBundleTokenAccount,
funderKeypair.publicKey,
1,
);
await tx.buildAndExecute();
const positionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true);
checkBitmapIsClosed(positionBundle!, 0);
});
it("successfully closes bundled position even if delegation exists", async () => {
const positionBundleInfo = await initializePositionBundle(ctx);
const bundleIndex = 0;
const positionInitInfo = await openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex
);
const tx = toTx(
ctx,
WhirlpoolIx.closeBundledPositionIx(ctx.program, {
bundledPosition: positionInitInfo.params.bundledPositionPda.publicKey,
bundleIndex,
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleAuthority: ctx.wallet.publicKey,
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount,
receiver: ctx.wallet.publicKey,
})
);
// delegate 1 token from ctx.wallet to funder
await approveToken(
provider,
positionBundleInfo.positionBundleTokenAccount,
funderKeypair.publicKey,
1,
);
// owner can close even if delegation exists
await tx.buildAndExecute();
const positionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true);
checkBitmapIsClosed(positionBundle!, 0);
});
it("should be faild: delegated amount is zero", async () => {
const positionBundleInfo = await initializePositionBundle(ctx);
const bundleIndex = 0;
const positionInitInfo = await openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex
);
const tx = toTx(
ctx,
WhirlpoolIx.closeBundledPositionIx(ctx.program, {
bundledPosition: positionInitInfo.params.bundledPositionPda.publicKey,
bundleIndex,
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleAuthority: funderKeypair.publicKey, // should be delegated
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount,
receiver: ctx.wallet.publicKey,
})
);
tx.addSigner(funderKeypair);
await assert.rejects(
tx.buildAndExecute(),
/0x1783/ // MissingOrInvalidDelegate
);
// delegate ZERO token from ctx.wallet to funder
await approveToken(
provider,
positionBundleInfo.positionBundleTokenAccount,
funderKeypair.publicKey,
0,
);
await assert.rejects(
tx.buildAndExecute(),
/0x1784/ // InvalidPositionTokenAmount
);
});
});
describe("transfer position bundle", () => {
it("successfully closes bundled position after position bundle token transfer", async () => {
const positionBundleInfo = await initializePositionBundle(ctx);
const bundleIndex = 0;
const positionInitInfo = await openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex
);
const funderATA = await createAssociatedTokenAccount(
provider,
positionBundleInfo.positionBundleMintKeypair.publicKey,
funderKeypair.publicKey,
ctx.wallet.publicKey,
);
await transfer(
provider,
positionBundleInfo.positionBundleTokenAccount,
funderATA,
1
);
const tokenInfo = await fetcher.getTokenInfo(funderATA, true);
assert.ok(tokenInfo?.amount.eqn(1));
const tx = toTx(
ctx,
WhirlpoolIx.closeBundledPositionIx(ctx.program, {
bundledPosition: positionInitInfo.params.bundledPositionPda.publicKey,
bundleIndex,
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleAuthority: funderKeypair.publicKey, // new owner
positionBundleTokenAccount: funderATA,
receiver: funderKeypair.publicKey
})
);
tx.addSigner(funderKeypair);
await tx.buildAndExecute();
const positionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true);
checkBitmapIsClosed(positionBundle!, 0);
});
});
describe("non-bundled position", () => {
it("should be failed: try to close NON-bundled position", async () => {
const positionBundleInfo = await initializePositionBundle(ctx);
const bundleIndex = 0;
const positionInitInfo = await openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex
);
// open NON-bundled position
const { params } = await openPosition(ctx, poolInitInfo.whirlpoolPda.publicKey, 0, 128);
const tx = toTx(
ctx,
WhirlpoolIx.closeBundledPositionIx(ctx.program, {
bundledPosition: params.positionPda.publicKey, // NON-bundled position
bundleIndex,
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleAuthority: ctx.wallet.publicKey,
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount,
receiver: ctx.wallet.publicKey,
})
);
await assert.rejects(
tx.buildAndExecute(),
/0x7d6/ // ConstraintSeeds (seed constraint was violated)
);
});
});
});

View File

@ -12,7 +12,7 @@ import {
ZERO_BN,
} from "../utils";
import { WhirlpoolTestFixture } from "../utils/fixture";
import { initTestPool, initTestPoolWithLiquidity, openPosition } from "../utils/init-utils";
import { initializePositionBundle, initTestPool, initTestPoolWithLiquidity, openBundledPosition, openPosition } from "../utils/init-utils";
describe("close_position", () => {
const provider = anchor.AnchorProvider.local();
@ -421,7 +421,45 @@ describe("close_position", () => {
positionTokenAccount: position.tokenAccount,
})
).buildAndExecute(),
/0x7dc/ // ConstraintAddress
// Seeds constraint added by adding PositionBundle, so ConstraintSeeds will be violated first
/0x7d6/ // ConstraintSeeds (seed constraint was violated)
);
});
describe("bundled position", () => {
it("fails if position is BUNDLED position", async () => {
const fixture = await new WhirlpoolTestFixture(ctx).init({
tickSpacing: TickSpacing.Standard,
positions: [],
});
const { poolInitInfo } = fixture.getInfos();
// open bundled position
const positionBundleInfo = await initializePositionBundle(ctx);
const bundleIndex = 0;
const positionInitInfo = await openBundledPosition(
ctx,
poolInitInfo.whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
0,
128,
);
// try to close bundled position
await assert.rejects(
toTx(
ctx,
WhirlpoolIx.closePositionIx(ctx.program, {
positionAuthority: provider.wallet.publicKey,
receiver: provider.wallet.publicKey,
position: positionInitInfo.params.bundledPositionPda.publicKey,
positionMint: positionBundleInfo.positionBundleMintKeypair.publicKey,
positionTokenAccount: positionBundleInfo.positionBundleTokenAccount,
})
).buildAndExecute(),
/0x7d6/ // ConstraintSeeds (seed constraint was violated)
);
});
});
});

View File

@ -0,0 +1,561 @@
import { PDA } from "@orca-so/common-sdk";
import * as anchor from "@project-serum/anchor";
import { ASSOCIATED_TOKEN_PROGRAM_ID, Token, TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { Keypair } from "@solana/web3.js";
import * as assert from "assert";
import { InitPoolParams, METADATA_PROGRAM_ADDRESS, PositionBundleData, POSITION_BUNDLE_SIZE, toTx, WhirlpoolIx } from "../../src";
import { WhirlpoolContext } from "../../src/context";
import {
approveToken,
createAssociatedTokenAccount,
ONE_SOL,
systemTransferTx,
TickSpacing,
transfer,
} from "../utils";
import { initializePositionBundle, initializePositionBundleWithMetadata, initTestPool, openBundledPosition } from "../utils/init-utils";
describe("delete_position_bundle", () => {
const provider = anchor.AnchorProvider.local(undefined, {
commitment: "confirmed",
preflightCommitment: "confirmed",
});
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Whirlpool;
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = ctx.fetcher;
const tickLowerIndex = 0;
const tickUpperIndex = 128;
let poolInitInfo: InitPoolParams;
let whirlpoolPda: PDA;
const funderKeypair = anchor.web3.Keypair.generate();
before(async () => {
poolInitInfo = (await initTestPool(ctx, TickSpacing.Standard)).poolInitInfo;
whirlpoolPda = poolInitInfo.whirlpoolPda;
await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute();
});
function checkBitmapIsOpened(account: PositionBundleData, bundleIndex: number): boolean {
if (bundleIndex < 0 || bundleIndex >= POSITION_BUNDLE_SIZE) throw Error("bundleIndex is out of bounds");
const bitmapIndex = Math.floor(bundleIndex / 8);
const bitmapOffset = bundleIndex % 8;
return (account.positionBitmap[bitmapIndex] & (1 << bitmapOffset)) > 0;
}
it("successfully closes an position bundle, with metadata", async () => {
// with local-validator, ctx.wallet may have large lamports and it overflows number data type...
const owner = funderKeypair;
const positionBundleInfo = await initializePositionBundleWithMetadata(
ctx,
owner.publicKey,
owner
);
// PositionBundle account exists
const prePositionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true);
assert.ok(prePositionBundle !== null);
// NFT supply should be 1
const preSupplyResponse = await provider.connection.getTokenSupply(positionBundleInfo.positionBundleMintKeypair.publicKey);
assert.equal(preSupplyResponse.value.uiAmount, 1);
// ATA account exists
assert.notEqual(await provider.connection.getAccountInfo(positionBundleInfo.positionBundleTokenAccount), undefined);
// Metadata account exists
assert.notEqual(await provider.connection.getAccountInfo(positionBundleInfo.positionBundleMetadataPda.publicKey), undefined);
const preBalance = await provider.connection.getBalance(owner.publicKey, "confirmed");
const rentPositionBundle = await provider.connection.getBalance(positionBundleInfo.positionBundlePda.publicKey, "confirmed");
const rentTokenAccount = await provider.connection.getBalance(positionBundleInfo.positionBundleTokenAccount, "confirmed");
await toTx(
ctx,
WhirlpoolIx.deletePositionBundleIx(ctx.program, {
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleMint: positionBundleInfo.positionBundleMintKeypair.publicKey,
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount,
owner: owner.publicKey,
receiver: owner.publicKey
})
).addSigner(owner).buildAndExecute();
const postBalance = await provider.connection.getBalance(owner.publicKey, "confirmed");
// PositionBundle account should be closed
const postPositionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true);
assert.ok(postPositionBundle === null);
// NFT should be burned and its supply should be 0
const supplyResponse = await provider.connection.getTokenSupply(positionBundleInfo.positionBundleMintKeypair.publicKey);
assert.equal(supplyResponse.value.uiAmount, 0);
// ATA account should be closed
assert.equal(await provider.connection.getAccountInfo(positionBundleInfo.positionBundleTokenAccount), undefined);
// Metadata account should NOT be closed
assert.notEqual(await provider.connection.getAccountInfo(positionBundleInfo.positionBundleMetadataPda.publicKey), undefined);
// check if rent are refunded
const diffBalance = postBalance - preBalance;
const rentTotal = rentPositionBundle + rentTokenAccount;
assert.equal(diffBalance, rentTotal);
});
it("successfully closes an position bundle, without metadata", async () => {
// with local-validator, ctx.wallet may have large lamports and it overflows number data type...
const owner = funderKeypair;
const positionBundleInfo = await initializePositionBundle(
ctx,
owner.publicKey,
owner
);
// PositionBundle account exists
const prePositionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true);
assert.ok(prePositionBundle !== null);
// NFT supply should be 1
const preSupplyResponse = await provider.connection.getTokenSupply(positionBundleInfo.positionBundleMintKeypair.publicKey);
assert.equal(preSupplyResponse.value.uiAmount, 1);
// ATA account exists
assert.notEqual(await provider.connection.getAccountInfo(positionBundleInfo.positionBundleTokenAccount), undefined);
const preBalance = await provider.connection.getBalance(owner.publicKey, "confirmed");
const rentPositionBundle = await provider.connection.getBalance(positionBundleInfo.positionBundlePda.publicKey, "confirmed");
const rentTokenAccount = await provider.connection.getBalance(positionBundleInfo.positionBundleTokenAccount, "confirmed");
await toTx(
ctx,
WhirlpoolIx.deletePositionBundleIx(ctx.program, {
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleMint: positionBundleInfo.positionBundleMintKeypair.publicKey,
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount,
owner: owner.publicKey,
receiver: owner.publicKey
})
).addSigner(owner).buildAndExecute();
const postBalance = await provider.connection.getBalance(owner.publicKey, "confirmed");
// PositionBundle account should be closed
const postPositionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true);
assert.ok(postPositionBundle === null);
// NFT should be burned and its supply should be 0
const supplyResponse = await provider.connection.getTokenSupply(positionBundleInfo.positionBundleMintKeypair.publicKey);
assert.equal(supplyResponse.value.uiAmount, 0);
// ATA account should be closed
assert.equal(await provider.connection.getAccountInfo(positionBundleInfo.positionBundleTokenAccount), undefined);
// check if rent are refunded
const diffBalance = postBalance - preBalance;
const rentTotal = rentPositionBundle + rentTokenAccount;
assert.equal(diffBalance, rentTotal);
});
it("successfully closes an position bundle, receiver != owner", async () => {
const receiver = funderKeypair;
const positionBundleInfo = await initializePositionBundle(
ctx,
ctx.wallet.publicKey,
);
const preBalance = await provider.connection.getBalance(receiver.publicKey, "confirmed");
const rentPositionBundle = await provider.connection.getBalance(positionBundleInfo.positionBundlePda.publicKey, "confirmed");
const rentTokenAccount = await provider.connection.getBalance(positionBundleInfo.positionBundleTokenAccount, "confirmed");
await toTx(
ctx,
WhirlpoolIx.deletePositionBundleIx(ctx.program, {
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleMint: positionBundleInfo.positionBundleMintKeypair.publicKey,
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount,
owner: ctx.wallet.publicKey,
receiver: receiver.publicKey
})
).buildAndExecute();
const postBalance = await provider.connection.getBalance(receiver.publicKey, "confirmed");
// check if rent are refunded to receiver
const diffBalance = postBalance - preBalance;
const rentTotal = rentPositionBundle + rentTokenAccount;
assert.equal(diffBalance, rentTotal);
});
it("should be failed: position bundle has opened bundled position (bundleIndex = 0)", async () => {
const positionBundleInfo = await initializePositionBundle(
ctx,
ctx.wallet.publicKey,
);
const bundleIndex = 0;
const positionInitInfo = await openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex
);
const { bundledPositionPda } = positionInitInfo.params;
const position = await fetcher.getPosition(positionInitInfo.params.bundledPositionPda.publicKey, true);
assert.equal(position!.tickLowerIndex, tickLowerIndex);
assert.equal(position!.tickUpperIndex, tickUpperIndex);
const positionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true);
checkBitmapIsOpened(positionBundle!, bundleIndex);
const tx = toTx(
ctx,
WhirlpoolIx.deletePositionBundleIx(ctx.program, {
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleMint: positionBundleInfo.positionBundleMintKeypair.publicKey,
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount,
owner: ctx.wallet.publicKey,
receiver: ctx.wallet.publicKey,
})
);
// should be failed
await assert.rejects(
tx.buildAndExecute(),
/0x179e/ // PositionBundleNotDeletable
);
// close bundled position
await toTx(
ctx,
WhirlpoolIx.closeBundledPositionIx(ctx.program, {
bundledPosition: bundledPositionPda.publicKey,
bundleIndex,
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleAuthority: ctx.wallet.publicKey,
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount,
receiver: ctx.wallet.publicKey,
})
).buildAndExecute();
// should be ok
await tx.buildAndExecute();
const deleted = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true);
assert.ok(deleted === null);
});
it("should be failed: position bundle has opened bundled position (bundleIndex = POSITION_BUNDLE_SIZE - 1)", async () => {
const positionBundleInfo = await initializePositionBundle(
ctx,
ctx.wallet.publicKey,
);
const bundleIndex = POSITION_BUNDLE_SIZE - 1;
const positionInitInfo = await openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex
);
const { bundledPositionPda } = positionInitInfo.params;
const position = await fetcher.getPosition(positionInitInfo.params.bundledPositionPda.publicKey, true);
assert.equal(position!.tickLowerIndex, tickLowerIndex);
assert.equal(position!.tickUpperIndex, tickUpperIndex);
const positionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true);
checkBitmapIsOpened(positionBundle!, bundleIndex);
const tx = toTx(
ctx,
WhirlpoolIx.deletePositionBundleIx(ctx.program, {
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleMint: positionBundleInfo.positionBundleMintKeypair.publicKey,
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount,
owner: ctx.wallet.publicKey,
receiver: ctx.wallet.publicKey,
})
);
// should be failed
await assert.rejects(
tx.buildAndExecute(),
/0x179e/ // PositionBundleNotDeletable
);
// close bundled position
await toTx(
ctx,
WhirlpoolIx.closeBundledPositionIx(ctx.program, {
bundledPosition: bundledPositionPda.publicKey,
bundleIndex,
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleAuthority: ctx.wallet.publicKey,
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount,
receiver: ctx.wallet.publicKey,
})
).buildAndExecute();
// should be ok
await tx.buildAndExecute();
const deleted = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true);
assert.ok(deleted === null);
});
it("should be failed: only owner can delete position bundle, delegated user cannot", async () => {
const positionBundleInfo = await initializePositionBundle(
ctx,
ctx.wallet.publicKey,
);
const delegate = Keypair.generate();
await approveToken(
provider,
positionBundleInfo.positionBundleTokenAccount,
delegate.publicKey,
1
);
const tx = toTx(
ctx,
WhirlpoolIx.deletePositionBundleIx(ctx.program, {
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleMint: positionBundleInfo.positionBundleMintKeypair.publicKey,
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount,
owner: delegate.publicKey, // not owner
receiver: ctx.wallet.publicKey,
})
).addSigner(delegate);
// should be failed
await assert.rejects(
tx.buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
// ownership transfer to delegate
const delegateTokenAccount = await createAssociatedTokenAccount(
provider,
positionBundleInfo.positionBundleMintKeypair.publicKey,
delegate.publicKey,
ctx.wallet.publicKey
);
await transfer(
provider,
positionBundleInfo.positionBundleTokenAccount,
delegateTokenAccount,
1
);
const txAfterTransfer = toTx(
ctx,
WhirlpoolIx.deletePositionBundleIx(ctx.program, {
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleMint: positionBundleInfo.positionBundleMintKeypair.publicKey,
positionBundleTokenAccount: delegateTokenAccount,
owner: delegate.publicKey, // now, delegate is owner
receiver: ctx.wallet.publicKey,
})
).addSigner(delegate);
await txAfterTransfer.buildAndExecute();
const deleted = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true);
assert.ok(deleted === null);
});
describe("invalid input account", () => {
it("should be failed: invalid position bundle", async () => {
const positionBundleInfo1 = await initializePositionBundle(
ctx,
ctx.wallet.publicKey,
);
const positionBundleInfo2 = await initializePositionBundle(
ctx,
ctx.wallet.publicKey,
);
const tx = toTx(
ctx,
WhirlpoolIx.deletePositionBundleIx(ctx.program, {
positionBundle: positionBundleInfo2.positionBundlePda.publicKey, // invalid
positionBundleMint: positionBundleInfo1.positionBundleMintKeypair.publicKey,
positionBundleTokenAccount: positionBundleInfo1.positionBundleTokenAccount,
owner: ctx.wallet.publicKey,
receiver: ctx.wallet.publicKey,
})
);
await assert.rejects(
tx.buildAndExecute(),
/0x7dc/ // ConstraintAddress
);
});
it("should be failed: invalid position bundle mint", async () => {
const positionBundleInfo1 = await initializePositionBundle(
ctx,
ctx.wallet.publicKey,
);
const positionBundleInfo2 = await initializePositionBundle(
ctx,
ctx.wallet.publicKey,
);
const tx = toTx(
ctx,
WhirlpoolIx.deletePositionBundleIx(ctx.program, {
positionBundle: positionBundleInfo1.positionBundlePda.publicKey,
positionBundleMint: positionBundleInfo2.positionBundleMintKeypair.publicKey, // invalid
positionBundleTokenAccount: positionBundleInfo1.positionBundleTokenAccount,
owner: ctx.wallet.publicKey,
receiver: ctx.wallet.publicKey,
})
);
await assert.rejects(
tx.buildAndExecute(),
/0x7dc/ // ConstraintAddress
);
});
it("should be failed: invalid ATA (amount is zero)", async () => {
const positionBundleInfo = await initializePositionBundle(
ctx,
ctx.wallet.publicKey,
);
// burn NFT
await toTx(ctx, {
instructions: [
Token.createBurnInstruction(
TOKEN_PROGRAM_ID,
positionBundleInfo.positionBundleMintKeypair.publicKey,
positionBundleInfo.positionBundleTokenAccount,
ctx.wallet.publicKey,
[],
1
)
],
cleanupInstructions: [],
signers: []
}).buildAndExecute();
const tokenAccount = await fetcher.getTokenInfo(positionBundleInfo.positionBundleTokenAccount);
assert.equal(tokenAccount!.amount.toString(), "0");
const tx = toTx(
ctx,
WhirlpoolIx.deletePositionBundleIx(ctx.program, {
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleMint: positionBundleInfo.positionBundleMintKeypair.publicKey,
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, // amount = 0
owner: ctx.wallet.publicKey,
receiver: ctx.wallet.publicKey,
})
);
await assert.rejects(
tx.buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
});
it("should be failed: invalid ATA (invalid mint)", async () => {
const positionBundleInfo1 = await initializePositionBundle(
ctx,
ctx.wallet.publicKey,
);
const positionBundleInfo2 = await initializePositionBundle(
ctx,
ctx.wallet.publicKey,
);
const tx = toTx(
ctx,
WhirlpoolIx.deletePositionBundleIx(ctx.program, {
positionBundle: positionBundleInfo1.positionBundlePda.publicKey,
positionBundleMint: positionBundleInfo1.positionBundleMintKeypair.publicKey,
positionBundleTokenAccount: positionBundleInfo2.positionBundleTokenAccount, // invalid,
owner: ctx.wallet.publicKey,
receiver: ctx.wallet.publicKey,
})
);
await assert.rejects(
tx.buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
});
it("should be failed: invalid ATA (invalid owner), invalid owner", async () => {
const positionBundleInfo = await initializePositionBundle(
ctx,
ctx.wallet.publicKey,
);
const otherWallet = Keypair.generate();
const tx = toTx(
ctx,
WhirlpoolIx.deletePositionBundleIx(ctx.program, {
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleMint: positionBundleInfo.positionBundleMintKeypair.publicKey,
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount, // ata.owner != owner
owner: otherWallet.publicKey,
receiver: ctx.wallet.publicKey,
})
).addSigner(otherWallet);
await assert.rejects(
tx.buildAndExecute(),
/0x7d3/ // ConstraintRaw
);
});
it("should be failed: invalid token program", async () => {
const positionBundleInfo = await initializePositionBundle(
ctx,
ctx.wallet.publicKey,
);
const ix = program.instruction.deletePositionBundle({
accounts: {
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleMint: positionBundleInfo.positionBundleMintKeypair.publicKey,
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount,
positionBundleOwner: ctx.wallet.publicKey,
tokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, // invalid
receiver: ctx.wallet.publicKey,
}
});
const tx = toTx(
ctx,
{
instructions: [ix],
cleanupInstructions: [],
signers: [],
}
);
await assert.rejects(
tx.buildAndExecute(),
/0xbc0/ // InvalidProgramId
);
});
});
});

View File

@ -1,4 +1,4 @@
import { MathUtil } from "@orca-so/common-sdk";
import { MathUtil, PDA } from "@orca-so/common-sdk";
import * as anchor from "@project-serum/anchor";
import * as assert from "assert";
import Decimal from "decimal.js";
@ -162,7 +162,7 @@ describe("initialize_pool", () => {
await assert.rejects(
toTx(ctx, WhirlpoolIx.initializePoolIx(ctx.program, modifiedPoolInitInfo)).buildAndExecute(),
/failed to complete|seeds|unauthorized/
/custom program error: 0x7d6/ // ConstraintSeeds
);
});
@ -177,7 +177,7 @@ describe("initialize_pool", () => {
await assert.rejects(
toTx(ctx, WhirlpoolIx.initializePoolIx(ctx.program, modifiedPoolInitInfo)).buildAndExecute(),
/failed to complete|seeds|unauthorized/
/custom program error: 0x7d6/ // ConstraintSeeds
);
});
@ -258,4 +258,29 @@ describe("initialize_pool", () => {
/custom program error: 0x177b/ // SqrtPriceOutOfBounds
);
});
it("ignore passed bump", async () => {
const { poolInitInfo } = await buildTestPoolParams(ctx, TickSpacing.Standard);
const whirlpoolPda = poolInitInfo.whirlpoolPda;
const validBump = whirlpoolPda.bump;
const invalidBump = (validBump + 1) % 256; // +1 shift mod 256
const modifiedWhirlpoolPda: PDA = {
publicKey: whirlpoolPda.publicKey,
bump: invalidBump,
};
const modifiedPoolInitInfo: InitPoolParams = {
...poolInitInfo,
whirlpoolPda: modifiedWhirlpoolPda,
};
await toTx(ctx, WhirlpoolIx.initializePoolIx(ctx.program, modifiedPoolInitInfo)).buildAndExecute();
// check if passed invalid bump was ignored
const whirlpool = (await fetcher.getPool(poolInitInfo.whirlpoolPda.publicKey)) as WhirlpoolData;
assert.equal(whirlpool.whirlpoolBump, validBump);
assert.notEqual(whirlpool.whirlpoolBump, invalidBump);
});
});

View File

@ -0,0 +1,267 @@
import { deriveATA } from "@orca-so/common-sdk";
import * as anchor from "@project-serum/anchor";
import { AccountInfo, ASSOCIATED_TOKEN_PROGRAM_ID, MintInfo, TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { Keypair, LAMPORTS_PER_SOL, PublicKey, SystemProgram } from "@solana/web3.js";
import * as assert from "assert";
import {
PDAUtil,
PositionBundleData,
POSITION_BUNDLE_SIZE,
toTx,
WhirlpoolContext,
} from "../../src";
import {
createMintInstructions,
mintToByAuthority,
} from "../utils";
import { initializePositionBundle } from "../utils/init-utils";
describe("initialize_position_bundle", () => {
const provider = anchor.AnchorProvider.local(undefined, {
commitment: "confirmed",
preflightCommitment: "confirmed",
});
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Whirlpool;
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = ctx.fetcher;
async function createInitializePositionBundleTx(ctx: WhirlpoolContext, overwrite: any, mintKeypair?: Keypair) {
const positionBundleMintKeypair = mintKeypair ?? Keypair.generate();
const positionBundlePda = PDAUtil.getPositionBundle(ctx.program.programId, positionBundleMintKeypair.publicKey);
const positionBundleTokenAccount = await deriveATA(ctx.wallet.publicKey, positionBundleMintKeypair.publicKey);
const defaultAccounts = {
positionBundle: positionBundlePda.publicKey,
positionBundleMint: positionBundleMintKeypair.publicKey,
positionBundleTokenAccount,
positionBundleOwner: ctx.wallet.publicKey,
funder: ctx.wallet.publicKey,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
};
const ix = program.instruction.initializePositionBundle({
accounts: {
...defaultAccounts,
...overwrite,
}
});
return toTx(ctx, {
instructions: [ix],
cleanupInstructions: [],
signers: [positionBundleMintKeypair],
});
}
async function checkPositionBundleMint(positionBundleMintPubkey: PublicKey) {
// verify position bundle Mint account
const positionBundleMint = (await ctx.fetcher.getMintInfo(positionBundleMintPubkey, true)) as MintInfo;
// should have NFT characteristics
assert.strictEqual(positionBundleMint.decimals, 0);
assert.ok(positionBundleMint.supply.eqn(1));
// mint auth & freeze auth should be set to None
assert.ok(positionBundleMint.mintAuthority === null);
assert.ok(positionBundleMint.freezeAuthority === null);
}
async function checkPositionBundleTokenAccount(positionBundleTokenAccountPubkey: PublicKey, owner: PublicKey, positionBundleMintPubkey: PublicKey) {
// verify position bundle Token account
const positionBundleTokenAccount = (await ctx.fetcher.getTokenInfo(positionBundleTokenAccountPubkey, true)) as AccountInfo;
assert.ok(positionBundleTokenAccount.amount.eqn(1));
assert.ok(positionBundleTokenAccount.mint.equals(positionBundleMintPubkey));
assert.ok(positionBundleTokenAccount.owner.equals(owner));
}
async function checkPositionBundle(positionBundlePubkey: PublicKey, positionBundleMintPubkey: PublicKey) {
// verify PositionBundle account
const positionBundle = (await ctx.fetcher.getPositionBundle(positionBundlePubkey, true)) as PositionBundleData;
assert.ok(positionBundle.positionBundleMint.equals(positionBundleMintPubkey));
assert.strictEqual(positionBundle.positionBitmap.length * 8, POSITION_BUNDLE_SIZE);
for (const bitmap of positionBundle.positionBitmap) {
assert.strictEqual(bitmap, 0);
}
}
async function createOtherWallet(): Promise<Keypair> {
const keypair = Keypair.generate();
const signature = await provider.connection.requestAirdrop(keypair.publicKey, 100 * LAMPORTS_PER_SOL);
await provider.connection.confirmTransaction(signature, "confirmed");
return keypair;
}
it("successfully initialize position bundle and verify initialized account contents", async () => {
const positionBundleInfo = await initializePositionBundle(
ctx,
ctx.wallet.publicKey,
// funder = ctx.wallet.publicKey
);
const {
positionBundleMintKeypair,
positionBundlePda,
positionBundleTokenAccount,
} = positionBundleInfo;
await checkPositionBundleMint(positionBundleMintKeypair.publicKey);
await checkPositionBundleTokenAccount(positionBundleTokenAccount, ctx.wallet.publicKey, positionBundleMintKeypair.publicKey);
await checkPositionBundle(positionBundlePda.publicKey, positionBundleMintKeypair.publicKey);
});
it("successfully initialize when funder is different than account paying for transaction fee", async () => {
const preBalance = await ctx.connection.getBalance(ctx.wallet.publicKey);
const otherWallet = await createOtherWallet();
const positionBundleInfo = await initializePositionBundle(
ctx,
ctx.wallet.publicKey,
otherWallet,
);
const postBalance = await ctx.connection.getBalance(ctx.wallet.publicKey);
const diffBalance = preBalance - postBalance;
const minRent = await ctx.connection.getMinimumBalanceForRentExemption(0);
assert.ok(diffBalance < minRent); // ctx.wallet didn't pay any rent
const {
positionBundleMintKeypair,
positionBundlePda,
positionBundleTokenAccount,
} = positionBundleInfo;
await checkPositionBundleMint(positionBundleMintKeypair.publicKey);
await checkPositionBundleTokenAccount(positionBundleTokenAccount, ctx.wallet.publicKey, positionBundleMintKeypair.publicKey);
await checkPositionBundle(positionBundlePda.publicKey, positionBundleMintKeypair.publicKey);
});
it("PositionBundle account has reserved space", async () => {
const positionBundleAccountSizeIncludingReserve = 8 + 32 + 32 + 64;
const positionBundleInfo = await initializePositionBundle(
ctx,
ctx.wallet.publicKey,
);
const account = await ctx.connection.getAccountInfo(positionBundleInfo.positionBundlePda.publicKey, "confirmed");
assert.equal(account!.data.length, positionBundleAccountSizeIncludingReserve);
});
it("should be failed: cannot mint additional NFT by owner", async () => {
const positionBundleInfo = await initializePositionBundle(
ctx,
ctx.wallet.publicKey,
);
await assert.rejects(
mintToByAuthority(
provider,
positionBundleInfo.positionBundleMintKeypair.publicKey,
positionBundleInfo.positionBundleTokenAccount,
1
),
/0x5/ // the total supply of this token is fixed
);
});
it("should be failed: already used mint is passed as position bundle mint", async () => {
const positionBundleMintKeypair = Keypair.generate();
// create mint
const createMintIx = await createMintInstructions(
provider,
ctx.wallet.publicKey,
positionBundleMintKeypair.publicKey
);
const createMintTx = toTx(ctx, {
instructions: createMintIx,
cleanupInstructions: [],
signers: [positionBundleMintKeypair]
});
await createMintTx.buildAndExecute();
const tx = await createInitializePositionBundleTx(ctx, {}, positionBundleMintKeypair);
await assert.rejects(
tx.buildAndExecute(),
(err) => { return JSON.stringify(err).includes("already in use") }
);
});
describe("invalid input account", () => {
it("should be failed: invalid position bundle address", async () => {
const tx = await createInitializePositionBundleTx(ctx, {
// invalid parameter
positionBundle: PDAUtil.getPositionBundle(ctx.program.programId, Keypair.generate().publicKey).publicKey,
});
await assert.rejects(
tx.buildAndExecute(),
/0x7d6/ // ConstraintSeeds
);
});
it("should be failed: invalid ATA address", async () => {
const tx = await createInitializePositionBundleTx(ctx, {
// invalid parameter
positionBundleTokenAccount: await deriveATA(ctx.wallet.publicKey, Keypair.generate().publicKey),
});
await assert.rejects(
tx.buildAndExecute(),
/An account required by the instruction is missing/ // Anchor cannot create derived ATA
);
});
it("should be failed: invalid token program", async () => {
const tx = await createInitializePositionBundleTx(ctx, {
// invalid parameter
tokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
});
await assert.rejects(
tx.buildAndExecute(),
/0xbc0/ // InvalidProgramId
);
});
it("should be failed: invalid system program", async () => {
const tx = await createInitializePositionBundleTx(ctx, {
// invalid parameter
systemProgram: TOKEN_PROGRAM_ID,
});
await assert.rejects(
tx.buildAndExecute(),
/0xbc0/ // InvalidProgramId
);
});
it("should be failed: invalid rent sysvar", async () => {
const tx = await createInitializePositionBundleTx(ctx, {
// invalid parameter
rent: anchor.web3.SYSVAR_CLOCK_PUBKEY,
});
await assert.rejects(
tx.buildAndExecute(),
/0xbc7/ // AccountSysvarMismatch
);
});
it("should be failed: invalid associated token program", async () => {
const tx = await createInitializePositionBundleTx(ctx, {
// invalid parameter
associatedTokenProgram: TOKEN_PROGRAM_ID,
});
await assert.rejects(
tx.buildAndExecute(),
/0xbc0/ // InvalidProgramId
);
});
});
});

View File

@ -0,0 +1,336 @@
import { Metadata } from "@metaplex-foundation/mpl-token-metadata";
import { deriveATA, PDA } from "@orca-so/common-sdk";
import * as anchor from "@project-serum/anchor";
import { AccountInfo, ASSOCIATED_TOKEN_PROGRAM_ID, MintInfo, TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { Keypair, LAMPORTS_PER_SOL, PublicKey, SystemProgram } from "@solana/web3.js";
import * as assert from "assert";
import {
METADATA_PROGRAM_ADDRESS,
PDAUtil,
PositionBundleData,
POSITION_BUNDLE_SIZE,
toTx,
WhirlpoolContext,
WHIRLPOOL_NFT_UPDATE_AUTH,
} from "../../src";
import {
createMintInstructions,
mintToByAuthority,
} from "../utils";
import { initializePositionBundleWithMetadata } from "../utils/init-utils";
describe("initialize_position_bundle_with_metadata", () => {
const provider = anchor.AnchorProvider.local(undefined, {
commitment: "confirmed",
preflightCommitment: "confirmed",
});
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Whirlpool;
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = ctx.fetcher;
async function createInitializePositionBundleWithMetadataTx(ctx: WhirlpoolContext, overwrite: any, mintKeypair?: Keypair) {
const positionBundleMintKeypair = mintKeypair ?? Keypair.generate();
const positionBundlePda = PDAUtil.getPositionBundle(ctx.program.programId, positionBundleMintKeypair.publicKey);
const positionBundleMetadataPda = PDAUtil.getPositionBundleMetadata(positionBundleMintKeypair.publicKey);
const positionBundleTokenAccount = await deriveATA(ctx.wallet.publicKey, positionBundleMintKeypair.publicKey);
const defaultAccounts = {
positionBundle: positionBundlePda.publicKey,
positionBundleMint: positionBundleMintKeypair.publicKey,
positionBundleMetadata: positionBundleMetadataPda.publicKey,
positionBundleTokenAccount,
positionBundleOwner: ctx.wallet.publicKey,
funder: ctx.wallet.publicKey,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
metadataProgram: METADATA_PROGRAM_ADDRESS,
metadataUpdateAuth: WHIRLPOOL_NFT_UPDATE_AUTH,
};
const ix = program.instruction.initializePositionBundleWithMetadata({
accounts: {
...defaultAccounts,
...overwrite,
}
});
return toTx(ctx, {
instructions: [ix],
cleanupInstructions: [],
signers: [positionBundleMintKeypair],
});
}
async function checkPositionBundleMint(positionBundleMintPubkey: PublicKey) {
// verify position bundle Mint account
const positionBundleMint = (await ctx.fetcher.getMintInfo(positionBundleMintPubkey, true)) as MintInfo;
// should have NFT characteristics
assert.strictEqual(positionBundleMint.decimals, 0);
assert.ok(positionBundleMint.supply.eqn(1));
// mint auth & freeze auth should be set to None
assert.ok(positionBundleMint.mintAuthority === null);
assert.ok(positionBundleMint.freezeAuthority === null);
}
async function checkPositionBundleTokenAccount(positionBundleTokenAccountPubkey: PublicKey, owner: PublicKey, positionBundleMintPubkey: PublicKey) {
// verify position bundle Token account
const positionBundleTokenAccount = (await ctx.fetcher.getTokenInfo(positionBundleTokenAccountPubkey, true)) as AccountInfo;
assert.ok(positionBundleTokenAccount.amount.eqn(1));
assert.ok(positionBundleTokenAccount.mint.equals(positionBundleMintPubkey));
assert.ok(positionBundleTokenAccount.owner.equals(owner));
}
async function checkPositionBundle(positionBundlePubkey: PublicKey, positionBundleMintPubkey: PublicKey) {
// verify PositionBundle account
const positionBundle = (await ctx.fetcher.getPositionBundle(positionBundlePubkey, true)) as PositionBundleData;
assert.ok(positionBundle.positionBundleMint.equals(positionBundleMintPubkey));
assert.strictEqual(positionBundle.positionBitmap.length * 8, POSITION_BUNDLE_SIZE);
for (const bitmap of positionBundle.positionBitmap) {
assert.strictEqual(bitmap, 0);
}
}
async function checkPositionBundleMetadata(metadataPda: PDA, positionMint: PublicKey) {
const WPB_METADATA_NAME_PREFIX = "Orca Position Bundle";
const WPB_METADATA_SYMBOL = "OPB";
const WPB_METADATA_URI = "https://arweave.net/A_Wo8dx2_3lSUwMIi7bdT_sqxi8soghRNAWXXiqXpgE";
const mintAddress = positionMint.toBase58();
const nftName = WPB_METADATA_NAME_PREFIX
+ " "
+ mintAddress.slice(0, 4)
+ "..."
+ mintAddress.slice(-4);
assert.ok(metadataPda != null);
const metadata = await Metadata.load(provider.connection, metadataPda.publicKey);
assert.ok(metadata.data.mint === positionMint.toString());
assert.ok(metadata.data.updateAuthority === WHIRLPOOL_NFT_UPDATE_AUTH.toBase58());
assert.ok(metadata.data.isMutable);
assert.strictEqual(metadata.data.data.name, nftName);
assert.strictEqual(metadata.data.data.symbol, WPB_METADATA_SYMBOL);
assert.strictEqual(metadata.data.data.uri, WPB_METADATA_URI);
}
async function createOtherWallet(): Promise<Keypair> {
const keypair = Keypair.generate();
const signature = await provider.connection.requestAirdrop(keypair.publicKey, 100 * LAMPORTS_PER_SOL);
await provider.connection.confirmTransaction(signature, "confirmed");
return keypair;
}
it("successfully initialize position bundle and verify initialized account contents", async () => {
const positionBundleInfo = await initializePositionBundleWithMetadata(
ctx,
ctx.wallet.publicKey,
// funder = ctx.wallet.publicKey
);
const {
positionBundleMintKeypair,
positionBundlePda,
positionBundleMetadataPda,
positionBundleTokenAccount,
} = positionBundleInfo;
await checkPositionBundleMint(positionBundleMintKeypair.publicKey);
await checkPositionBundleTokenAccount(positionBundleTokenAccount, ctx.wallet.publicKey, positionBundleMintKeypair.publicKey);
await checkPositionBundle(positionBundlePda.publicKey, positionBundleMintKeypair.publicKey);
await checkPositionBundleMetadata(positionBundleMetadataPda, positionBundleMintKeypair.publicKey);
});
it("successfully initialize when funder is different than account paying for transaction fee", async () => {
const preBalance = await ctx.connection.getBalance(ctx.wallet.publicKey);
const otherWallet = await createOtherWallet();
const positionBundleInfo = await initializePositionBundleWithMetadata(
ctx,
ctx.wallet.publicKey,
otherWallet,
);
const postBalance = await ctx.connection.getBalance(ctx.wallet.publicKey);
const diffBalance = preBalance - postBalance;
const minRent = await ctx.connection.getMinimumBalanceForRentExemption(0);
assert.ok(diffBalance < minRent); // ctx.wallet didn't pay any rent
const {
positionBundleMintKeypair,
positionBundlePda,
positionBundleMetadataPda,
positionBundleTokenAccount,
} = positionBundleInfo;
await checkPositionBundleMint(positionBundleMintKeypair.publicKey);
await checkPositionBundleTokenAccount(positionBundleTokenAccount, ctx.wallet.publicKey, positionBundleMintKeypair.publicKey);
await checkPositionBundle(positionBundlePda.publicKey, positionBundleMintKeypair.publicKey);
await checkPositionBundleMetadata(positionBundleMetadataPda, positionBundleMintKeypair.publicKey);
});
it("PositionBundle account has reserved space", async () => {
const positionBundleAccountSizeIncludingReserve = 8 + 32 + 32 + 64;
const positionBundleInfo = await initializePositionBundleWithMetadata(
ctx,
ctx.wallet.publicKey,
);
const account = await ctx.connection.getAccountInfo(positionBundleInfo.positionBundlePda.publicKey, "confirmed");
assert.equal(account!.data.length, positionBundleAccountSizeIncludingReserve);
});
it("should be failed: cannot mint additional NFT by owner", async () => {
const positionBundleInfo = await initializePositionBundleWithMetadata(
ctx,
ctx.wallet.publicKey,
);
await assert.rejects(
mintToByAuthority(
provider,
positionBundleInfo.positionBundleMintKeypair.publicKey,
positionBundleInfo.positionBundleTokenAccount,
1
),
/0x5/ // the total supply of this token is fixed
);
});
it("should be failed: already used mint is passed as position bundle mint", async () => {
const positionBundleMintKeypair = Keypair.generate();
// create mint
const createMintIx = await createMintInstructions(
provider,
ctx.wallet.publicKey,
positionBundleMintKeypair.publicKey
);
const createMintTx = toTx(ctx, {
instructions: createMintIx,
cleanupInstructions: [],
signers: [positionBundleMintKeypair]
});
await createMintTx.buildAndExecute();
const tx = await createInitializePositionBundleWithMetadataTx(ctx, {}, positionBundleMintKeypair);
await assert.rejects(
tx.buildAndExecute(),
(err) => { return JSON.stringify(err).includes("already in use") }
);
});
describe("invalid input account", () => {
it("should be failed: invalid position bundle address", async () => {
const tx = await createInitializePositionBundleWithMetadataTx(ctx, {
// invalid parameter
positionBundle: PDAUtil.getPositionBundle(ctx.program.programId, Keypair.generate().publicKey).publicKey,
});
await assert.rejects(
tx.buildAndExecute(),
/0x7d6/ // ConstraintSeeds
);
});
it("should be failed: invalid metadata address", async () => {
const tx = await createInitializePositionBundleWithMetadataTx(ctx, {
// invalid parameter
positionBundleMetadata: PDAUtil.getPositionBundleMetadata(Keypair.generate().publicKey).publicKey,
});
await assert.rejects(
tx.buildAndExecute(),
/0x5/ // InvalidMetadataKey: cannot create Metadata
);
});
it("should be failed: invalid ATA address", async () => {
const tx = await createInitializePositionBundleWithMetadataTx(ctx, {
// invalid parameter
positionBundleTokenAccount: await deriveATA(ctx.wallet.publicKey, Keypair.generate().publicKey),
});
await assert.rejects(
tx.buildAndExecute(),
/An account required by the instruction is missing/ // Anchor cannot create derived ATA
);
});
it("should be failed: invalid update auth", async () => {
const tx = await createInitializePositionBundleWithMetadataTx(ctx, {
// invalid parameter
metadataUpdateAuth: Keypair.generate().publicKey,
});
await assert.rejects(
tx.buildAndExecute(),
/0x7dc/ // ConstraintAddress
);
});
it("should be failed: invalid token program", async () => {
const tx = await createInitializePositionBundleWithMetadataTx(ctx, {
// invalid parameter
tokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
});
await assert.rejects(
tx.buildAndExecute(),
/0xbc0/ // InvalidProgramId
);
});
it("should be failed: invalid system program", async () => {
const tx = await createInitializePositionBundleWithMetadataTx(ctx, {
// invalid parameter
systemProgram: TOKEN_PROGRAM_ID,
});
await assert.rejects(
tx.buildAndExecute(),
/0xbc0/ // InvalidProgramId
);
});
it("should be failed: invalid rent sysvar", async () => {
const tx = await createInitializePositionBundleWithMetadataTx(ctx, {
// invalid parameter
rent: anchor.web3.SYSVAR_CLOCK_PUBKEY,
});
await assert.rejects(
tx.buildAndExecute(),
/0xbc7/ // AccountSysvarMismatch
);
});
it("should be failed: invalid associated token program", async () => {
const tx = await createInitializePositionBundleWithMetadataTx(ctx, {
// invalid parameter
associatedTokenProgram: TOKEN_PROGRAM_ID,
});
await assert.rejects(
tx.buildAndExecute(),
/0xbc0/ // InvalidProgramId
);
});
it("should be failed: invalid metadata program", async () => {
const tx = await createInitializePositionBundleWithMetadataTx(ctx, {
// invalid parameter
metadataProgram: TOKEN_PROGRAM_ID,
});
await assert.rejects(
tx.buildAndExecute(),
/0x7dc/ // ConstraintAddress
);
});
});
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,613 @@
import { PDA } from "@orca-so/common-sdk";
import * as anchor from "@project-serum/anchor";
import { ASSOCIATED_TOKEN_PROGRAM_ID, Token, TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { PublicKey, SystemProgram } from "@solana/web3.js";
import * as assert from "assert";
import {
InitPoolParams,
MAX_TICK_INDEX,
MIN_TICK_INDEX,
PDAUtil,
PositionBundleData,
PositionData,
POSITION_BUNDLE_SIZE,
toTx,
WhirlpoolContext,
WhirlpoolIx,
} from "../../src";
import {
approveToken,
createAssociatedTokenAccount,
ONE_SOL,
systemTransferTx,
TickSpacing,
transfer,
ZERO_BN,
} from "../utils";
import { initializePositionBundle, initTestPool, openBundledPosition } from "../utils/init-utils";
describe("open_bundled_position", () => {
const provider = anchor.AnchorProvider.local(undefined, {
commitment: "confirmed",
preflightCommitment: "confirmed",
});
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Whirlpool;
const ctx = WhirlpoolContext.fromWorkspace(provider, program);
const fetcher = ctx.fetcher;
const tickLowerIndex = 0;
const tickUpperIndex = 128;
let poolInitInfo: InitPoolParams;
let whirlpoolPda: PDA;
const funderKeypair = anchor.web3.Keypair.generate();
before(async () => {
poolInitInfo = (await initTestPool(ctx, TickSpacing.Standard)).poolInitInfo;
whirlpoolPda = poolInitInfo.whirlpoolPda;
await systemTransferTx(provider, funderKeypair.publicKey, ONE_SOL).buildAndExecute();
});
async function createOpenBundledPositionTx(
ctx: WhirlpoolContext,
positionBundleMint: PublicKey,
bundleIndex: number,
overwrite: any,
) {
const bundledPositionPda = PDAUtil.getBundledPosition(ctx.program.programId, positionBundleMint, bundleIndex);
const positionBundle = PDAUtil.getPositionBundle(ctx.program.programId, positionBundleMint).publicKey;
const positionBundleTokenAccount = await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
positionBundleMint,
ctx.wallet.publicKey
);
const defaultAccounts = {
bundledPosition: bundledPositionPda.publicKey,
positionBundle,
positionBundleTokenAccount,
positionBundleAuthority: ctx.wallet.publicKey,
whirlpool: whirlpoolPda.publicKey,
funder: ctx.wallet.publicKey,
systemProgram: SystemProgram.programId,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
};
const ix = program.instruction.openBundledPosition(bundleIndex, tickLowerIndex, tickUpperIndex, {
accounts: {
...defaultAccounts,
...overwrite,
}
});
return toTx(ctx, {
instructions: [ix],
cleanupInstructions: [],
signers: [],
});
}
function checkPositionAccountContents(position: PositionData, mint: PublicKey) {
assert.strictEqual(position.tickLowerIndex, tickLowerIndex);
assert.strictEqual(position.tickUpperIndex, tickUpperIndex);
assert.ok(position.whirlpool.equals(poolInitInfo.whirlpoolPda.publicKey));
assert.ok(position.positionMint.equals(mint));
assert.ok(position.liquidity.eq(ZERO_BN));
assert.ok(position.feeGrowthCheckpointA.eq(ZERO_BN));
assert.ok(position.feeGrowthCheckpointB.eq(ZERO_BN));
assert.ok(position.feeOwedA.eq(ZERO_BN));
assert.ok(position.feeOwedB.eq(ZERO_BN));
assert.ok(position.rewardInfos[0].amountOwed.eq(ZERO_BN));
assert.ok(position.rewardInfos[1].amountOwed.eq(ZERO_BN));
assert.ok(position.rewardInfos[2].amountOwed.eq(ZERO_BN));
assert.ok(position.rewardInfos[0].growthInsideCheckpoint.eq(ZERO_BN));
assert.ok(position.rewardInfos[1].growthInsideCheckpoint.eq(ZERO_BN));
assert.ok(position.rewardInfos[2].growthInsideCheckpoint.eq(ZERO_BN));
}
function checkBitmapIsOpened(account: PositionBundleData, bundleIndex: number): boolean {
if (bundleIndex < 0 || bundleIndex >= POSITION_BUNDLE_SIZE) throw Error("bundleIndex is out of bounds");
const bitmapIndex = Math.floor(bundleIndex / 8);
const bitmapOffset = bundleIndex % 8;
return (account.positionBitmap[bitmapIndex] & (1 << bitmapOffset)) > 0;
}
function checkBitmapIsClosed(account: PositionBundleData, bundleIndex: number): boolean {
if (bundleIndex < 0 || bundleIndex >= POSITION_BUNDLE_SIZE) throw Error("bundleIndex is out of bounds");
const bitmapIndex = Math.floor(bundleIndex / 8);
const bitmapOffset = bundleIndex % 8;
return (account.positionBitmap[bitmapIndex] & (1 << bitmapOffset)) === 0;
}
function checkBitmap(account: PositionBundleData, openedBundleIndexes: number[]) {
for (let i=0; i<POSITION_BUNDLE_SIZE; i++) {
if (openedBundleIndexes.includes(i)) {
assert.ok(checkBitmapIsOpened(account, i));
}
else {
assert.ok(checkBitmapIsClosed(account, i));
}
}
}
it("successfully opens bundled position and verify position address contents", async () => {
const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey);
const bundleIndex = 0;
const positionInitInfo = await openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex
);
const { bundledPositionPda } = positionInitInfo.params;
const position = (await fetcher.getPosition(bundledPositionPda.publicKey)) as PositionData;
checkPositionAccountContents(position, positionBundleInfo.positionBundleMintKeypair.publicKey);
const positionBundle = (await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true)) as PositionBundleData;
checkBitmap(positionBundle, [bundleIndex]);
});
it("successfully opens bundled position when funder is different than account paying for transaction fee", async () => {
const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey);
const preBalance = await ctx.connection.getBalance(ctx.wallet.publicKey);
const bundleIndex = POSITION_BUNDLE_SIZE - 1;
const positionInitInfo = await openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex,
ctx.wallet.publicKey,
funderKeypair,
);
const { bundledPositionPda } = positionInitInfo.params;
const postBalance = await ctx.connection.getBalance(ctx.wallet.publicKey);
const diffBalance = preBalance - postBalance;
const minRent = await ctx.connection.getMinimumBalanceForRentExemption(0);
assert.ok(diffBalance < minRent); // ctx.wallet didn't any rent
const position = (await fetcher.getPosition(bundledPositionPda.publicKey)) as PositionData;
checkPositionAccountContents(position, positionBundleInfo.positionBundleMintKeypair.publicKey);
const positionBundle = (await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true)) as PositionBundleData;
checkBitmap(positionBundle, [bundleIndex]);
});
it("successfully opens multiple bundled position and verify bitmap", async () => {
const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey);
const bundleIndexes = [1, 7, 8, 64, 127, 128, 254, 255];
for (const bundleIndex of bundleIndexes) {
const positionInitInfo = await openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex
);
const { bundledPositionPda } = positionInitInfo.params;
const position = (await fetcher.getPosition(bundledPositionPda.publicKey)) as PositionData;
checkPositionAccountContents(position, positionBundleInfo.positionBundleMintKeypair.publicKey);
}
const positionBundle = (await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true)) as PositionBundleData;
checkBitmap(positionBundle, bundleIndexes);
});
describe("invalid bundle index", () => {
it("should be failed: invalid bundle index (< 0)", async () => {
const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey);
const bundleIndex = -1;
await assert.rejects(
openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex,
),
/It must be >= 0 and <= 65535/ // rejected by client
);
});
it("should be failed: invalid bundle index (POSITION_BUNDLE_SIZE)", async () => {
const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey);
const bundleIndex = POSITION_BUNDLE_SIZE;
await assert.rejects(
openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex,
),
/0x179b/ // InvalidBundleIndex
);
});
it("should be failed: invalid bundle index (u16 max)", async () => {
const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey);
const bundleIndex = 2**16 - 1;
await assert.rejects(
openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex,
),
/0x179b/ // InvalidBundleIndex
);
});
});
describe("invalid tick index", () => {
async function assertTicksFail(lowerTick: number, upperTick: number) {
const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey);
const bundleIndex = 0;
await assert.rejects(
openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
lowerTick,
upperTick,
provider.wallet.publicKey,
funderKeypair
),
/0x177a/ // InvalidTickIndex
);
}
it("should be failed: user pass in an out of bound tick index for upper-index", async () => {
await assertTicksFail(0, MAX_TICK_INDEX + 1);
});
it("should be failed: user pass in a lower tick index that is higher than the upper-index", async () => {
await assertTicksFail(-22534, -22534 - 1);
});
it("should be failed: user pass in a lower tick index that equals the upper-index", async () => {
await assertTicksFail(22365, 22365);
});
it("should be failed: user pass in an out of bound tick index for lower-index", async () => {
await assertTicksFail(MIN_TICK_INDEX - 1, 0);
});
it("should be failed: user pass in a non-initializable tick index for upper-index", async () => {
await assertTicksFail(0, 1);
});
it("should be failed: user pass in a non-initializable tick index for lower-index", async () => {
await assertTicksFail(1, 2);
});
});
it("should be fail: user opens bundled position with bundle index whose state is opened", async () => {
const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey);
const bundleIndex = 0;
await openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex
);
const positionBundle = (await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true)) as PositionBundleData;
assert.ok(checkBitmapIsOpened(positionBundle, bundleIndex));
await assert.rejects(
openBundledPosition(
ctx,
whirlpoolPda.publicKey,
positionBundleInfo.positionBundleMintKeypair.publicKey,
bundleIndex,
tickLowerIndex,
tickUpperIndex
),
(err) => { return JSON.stringify(err).includes("already in use") }
);
});
describe("invalid input account", () => {
it("should be failed: invalid bundled position", async () => {
const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey);
const tx = await createOpenBundledPositionTx(
ctx, positionBundleInfo.positionBundleMintKeypair.publicKey, 0, {
// invalid parameter
bundledPosition: PDAUtil.getBundledPosition(
ctx.program.programId,
positionBundleInfo.positionBundleMintKeypair.publicKey,
1 // another bundle index
).publicKey
}
);
await assert.rejects(
tx.buildAndExecute(),
/0x7d6/ // ConstraintSeeds
);
});
it("should be failed: invalid position bundle", async () => {
const positionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey);
const otherPositionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey);
const tx = await createOpenBundledPositionTx(
ctx, positionBundleInfo.positionBundleMintKeypair.publicKey, 0, {
// invalid parameter
positionBundle: otherPositionBundleInfo.positionBundlePda.publicKey,
}
);
await assert.rejects(
tx.buildAndExecute(),
/0x7d6/ // ConstraintSeeds
);
});
it("should be failed: invalid ATA (amount is zero)", async () => {
const positionBundleInfo = await initializePositionBundle(ctx, funderKeypair.publicKey, funderKeypair);
const ata = await createAssociatedTokenAccount(
provider,
positionBundleInfo.positionBundleMintKeypair.publicKey,
ctx.wallet.publicKey,
ctx.wallet.publicKey
);
const tx = await createOpenBundledPositionTx(
ctx, positionBundleInfo.positionBundleMintKeypair.publicKey, 0, {
// invalid parameter
positionBundleTokenAccount: ata,
}
);
await assert.rejects(
tx.buildAndExecute(),
/0x7d3/ // ConstraintRaw (amount == 1)
);
});
it("should be failed: invalid ATA (invalid mint)", async () => {
const positionBundleInfo = await initializePositionBundle(ctx, funderKeypair.publicKey, funderKeypair);
const otherPositionBundleInfo = await initializePositionBundle(ctx, ctx.wallet.publicKey);
const tx = await createOpenBundledPositionTx(
ctx, positionBundleInfo.positionBundleMintKeypair.publicKey, 0, {
// invalid parameter
positionBundleTokenAccount: otherPositionBundleInfo.positionBundleTokenAccount,
}
);
await assert.rejects(
tx.buildAndExecute(),
/0x7d3/ // ConstraintRaw (mint == position_bundle.position_bundle_mint)
);
});
it("should be failed: invalid position bundle authority", async () => {
const positionBundleInfo = await initializePositionBundle(ctx, funderKeypair.publicKey, funderKeypair);
const tx = await createOpenBundledPositionTx(
ctx, positionBundleInfo.positionBundleMintKeypair.publicKey, 0, {
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount,
// invalid parameter
positionBundleAuthority: ctx.wallet.publicKey,
}
);
await assert.rejects(
tx.buildAndExecute(),
/0x1783/ // MissingOrInvalidDelegate
);
});
it("should be failed: invalid whirlpool", async () => {
const positionBundleInfo = await initializePositionBundle(ctx);
const tx = await createOpenBundledPositionTx(
ctx, positionBundleInfo.positionBundleMintKeypair.publicKey, 0, {
// invalid parameter
whirlpool: positionBundleInfo.positionBundlePda.publicKey,
}
);
await assert.rejects(
tx.buildAndExecute(),
/0xbba/ // AccountDiscriminatorMismatch
);
});
it("should be failed: invalid system program", async () => {
const positionBundleInfo = await initializePositionBundle(ctx);
const tx = await createOpenBundledPositionTx(
ctx, positionBundleInfo.positionBundleMintKeypair.publicKey, 0, {
// invalid parameter
systemProgram: TOKEN_PROGRAM_ID,
}
);
await assert.rejects(
tx.buildAndExecute(),
/0xbc0/ // InvalidProgramId
);
});
it("should be failed: invalid rent", async () => {
const positionBundleInfo = await initializePositionBundle(ctx);
const tx = await createOpenBundledPositionTx(
ctx, positionBundleInfo.positionBundleMintKeypair.publicKey, 0, {
// invalid parameter
rent: anchor.web3.SYSVAR_CLOCK_PUBKEY,
}
);
await assert.rejects(
tx.buildAndExecute(),
/0xbc7/ // AccountSysvarMismatch
);
});
});
describe("authority delegation", () => {
it("successfully opens bundled position with delegated authority", async () => {
const positionBundleInfo = await initializePositionBundle(ctx, funderKeypair.publicKey, funderKeypair);
const tx = await createOpenBundledPositionTx(
ctx, positionBundleInfo.positionBundleMintKeypair.publicKey, 0, {
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount,
positionBundleAuthority: ctx.wallet.publicKey,
}
);
await assert.rejects(
tx.buildAndExecute(),
/0x1783/ // MissingOrInvalidDelegate
);
// delegate 1 token from funder to ctx.wallet
await approveToken(
provider,
positionBundleInfo.positionBundleTokenAccount,
ctx.wallet.publicKey,
1,
funderKeypair
);
await tx.buildAndExecute();
const positionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true);
checkBitmapIsOpened(positionBundle!, 0);
});
it("successfully opens bundled position even if delegation exists", async () => {
const positionBundleInfo = await initializePositionBundle(ctx);
const tx = await createOpenBundledPositionTx(
ctx, positionBundleInfo.positionBundleMintKeypair.publicKey, 0, {
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount,
positionBundleAuthority: ctx.wallet.publicKey,
}
);
// delegate 1 token from ctx.wallet to funder
await approveToken(
provider,
positionBundleInfo.positionBundleTokenAccount,
funderKeypair.publicKey,
1,
);
// owner can open even if delegation exists
await tx.buildAndExecute();
const positionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true);
checkBitmapIsOpened(positionBundle!, 0);
});
it("should be failed: delegated amount is zero", async () => {
const positionBundleInfo = await initializePositionBundle(ctx, funderKeypair.publicKey, funderKeypair);
const tx = await createOpenBundledPositionTx(
ctx, positionBundleInfo.positionBundleMintKeypair.publicKey, 0, {
positionBundleTokenAccount: positionBundleInfo.positionBundleTokenAccount,
positionBundleAuthority: ctx.wallet.publicKey,
}
);
await assert.rejects(
tx.buildAndExecute(),
/0x1783/ // MissingOrInvalidDelegate
);
// delegate ZERO token from funder to ctx.wallet
await approveToken(
provider,
positionBundleInfo.positionBundleTokenAccount,
ctx.wallet.publicKey,
0,
funderKeypair
);
await assert.rejects(
tx.buildAndExecute(),
/0x1784/ // InvalidPositionTokenAmount
);
});
});
describe("transfer position bundle", () => {
it("successfully opens bundled position after position bundle token transfer", async () => {
const positionBundleInfo = await initializePositionBundle(ctx);
const funderATA = await createAssociatedTokenAccount(
provider,
positionBundleInfo.positionBundleMintKeypair.publicKey,
funderKeypair.publicKey,
ctx.wallet.publicKey,
);
await transfer(
provider,
positionBundleInfo.positionBundleTokenAccount,
funderATA,
1
);
const tokenInfo = await fetcher.getTokenInfo(funderATA, true);
assert.ok(tokenInfo?.amount.eqn(1));
const tx = toTx(
ctx,
WhirlpoolIx.openBundledPositionIx(ctx.program, {
bundledPositionPda: PDAUtil.getBundledPosition(ctx.program.programId, positionBundleInfo.positionBundleMintKeypair.publicKey, 0),
bundleIndex: 0,
funder: funderKeypair.publicKey,
positionBundle: positionBundleInfo.positionBundlePda.publicKey,
positionBundleAuthority: funderKeypair.publicKey,
positionBundleTokenAccount: funderATA,
tickLowerIndex,
tickUpperIndex,
whirlpool: whirlpoolPda.publicKey,
})
);
tx.addSigner(funderKeypair);
await tx.buildAndExecute();
const positionBundle = await fetcher.getPositionBundle(positionBundleInfo.positionBundlePda.publicKey, true);
checkBitmapIsOpened(positionBundle!, 0);
});
});
});

View File

@ -114,7 +114,9 @@ describe("set_fee_rate", () => {
},
signers: [configKeypairs.feeAuthorityKeypair],
}),
/A has_one constraint was violated/ // ConstraintHasOne
// message have been changed
// https://github.com/coral-xyz/anchor/pull/2101/files#diff-e564d6832afe5358ef129e96970ba1e5180b5e74aba761831e1923c06d7b839fR412
/A has[_ ]one constraint was violated/ // ConstraintHasOne
);
});

View File

@ -111,7 +111,9 @@ describe("set_protocol_fee_rate", () => {
},
signers: [configKeypairs.feeAuthorityKeypair],
}),
/A has_one constraint was violated/ // ConstraintHasOne
// message have been changed
// https://github.com/coral-xyz/anchor/pull/2101/files#diff-e564d6832afe5358ef129e96970ba1e5180b5e74aba761831e1923c06d7b839fR412
/A has[_ ]one constraint was violated/ // ConstraintHasOne
);
});

View File

@ -0,0 +1,149 @@
import * as assert from "assert";
import { PositionBundleUtil, POSITION_BUNDLE_SIZE } from "../../../../src";
import { buildPositionBundleData } from "../../../utils/testDataTypes";
describe("PositionBundleUtil tests", () => {
const occupiedEmpty: number[] = [];
const occupiedPartial: number[] = [0, 1, 5, 49, 128, 193, 255];
const occupiedFull: number[] = new Array(POSITION_BUNDLE_SIZE).fill(0).map((a, i) => i);
describe("checkBundleIndexInBounds", () => {
it("valid bundle indexes", async () => {
for (let bundleIndex=0; bundleIndex<POSITION_BUNDLE_SIZE; bundleIndex++) {
assert.ok(PositionBundleUtil.checkBundleIndexInBounds(bundleIndex));
}
});
it("less than zero", async () => {
assert.ok(!PositionBundleUtil.checkBundleIndexInBounds(-1));
});
it("greater than or equal to POSITION_BUNDLE_SIZE", async () => {
assert.ok(!PositionBundleUtil.checkBundleIndexInBounds(POSITION_BUNDLE_SIZE));
assert.ok(!PositionBundleUtil.checkBundleIndexInBounds(POSITION_BUNDLE_SIZE+1));
});
});
it("isOccupied / isUnoccupied", async () => {
const positionBundle = buildPositionBundleData(occupiedPartial);
for (let bundleIndex=0; bundleIndex<POSITION_BUNDLE_SIZE; bundleIndex++) {
if (occupiedPartial.includes(bundleIndex)) {
assert.ok(PositionBundleUtil.isOccupied(positionBundle, bundleIndex));
assert.ok(!PositionBundleUtil.isUnoccupied(positionBundle, bundleIndex));
}
else {
assert.ok(PositionBundleUtil.isUnoccupied(positionBundle, bundleIndex));
assert.ok(!PositionBundleUtil.isOccupied(positionBundle, bundleIndex));
}
}
});
describe("isFull / isEmpty", () => {
it("empty", async () => {
const positionBundle = buildPositionBundleData(occupiedEmpty);
assert.ok(PositionBundleUtil.isEmpty(positionBundle));
assert.ok(!PositionBundleUtil.isFull(positionBundle));
});
it("some bundle indexes are occupied", async () => {
const positionBundle = buildPositionBundleData(occupiedPartial);
assert.ok(!PositionBundleUtil.isEmpty(positionBundle));
assert.ok(!PositionBundleUtil.isFull(positionBundle));
});
it("full", async () => {
const positionBundle = buildPositionBundleData(occupiedFull);
assert.ok(!PositionBundleUtil.isEmpty(positionBundle));
assert.ok(PositionBundleUtil.isFull(positionBundle));
})
})
describe("getOccupiedBundleIndexes", () => {
it("empty", async () => {
const positionBundle = buildPositionBundleData(occupiedEmpty);
const result = PositionBundleUtil.getOccupiedBundleIndexes(positionBundle);
assert.equal(result.length, 0);
});
it("some bundle indexes are occupied", async () => {
const positionBundle = buildPositionBundleData(occupiedPartial);
const result = PositionBundleUtil.getOccupiedBundleIndexes(positionBundle);
assert.equal(result.length, occupiedPartial.length);
assert.ok(occupiedPartial.every(index => result.includes(index)));
});
it("full", async () => {
const positionBundle = buildPositionBundleData(occupiedFull);
const result = PositionBundleUtil.getOccupiedBundleIndexes(positionBundle);
assert.equal(result.length, POSITION_BUNDLE_SIZE);
assert.ok(occupiedFull.every(index => result.includes(index)));
})
});
describe("getUnoccupiedBundleIndexes", () => {
it("empty", async () => {
const positionBundle = buildPositionBundleData(occupiedEmpty);
const result = PositionBundleUtil.getUnoccupiedBundleIndexes(positionBundle);
assert.equal(result.length, POSITION_BUNDLE_SIZE);
assert.ok(occupiedFull.every(index => result.includes(index)));
});
it("some bundle indexes are occupied", async () => {
const positionBundle = buildPositionBundleData(occupiedPartial);
const result = PositionBundleUtil.getUnoccupiedBundleIndexes(positionBundle);
assert.equal(result.length, POSITION_BUNDLE_SIZE - occupiedPartial.length);
assert.ok(occupiedPartial.every(index => !result.includes(index)));
});
it("full", async () => {
const positionBundle = buildPositionBundleData(occupiedFull);
const result = PositionBundleUtil.getUnoccupiedBundleIndexes(positionBundle);
assert.equal(result.length, 0);
})
});
describe("findUnoccupiedBundleIndex", () => {
it("empty", async () => {
const positionBundle = buildPositionBundleData(occupiedEmpty);
const result = PositionBundleUtil.findUnoccupiedBundleIndex(positionBundle);
assert.equal(result, 0);
});
it("some bundle indexes are occupied", async () => {
const positionBundle = buildPositionBundleData(occupiedPartial);
const result = PositionBundleUtil.findUnoccupiedBundleIndex(positionBundle);
assert.equal(result, 2);
});
it("full", async () => {
const positionBundle = buildPositionBundleData(occupiedFull);
const result = PositionBundleUtil.findUnoccupiedBundleIndex(positionBundle);
assert.ok(result === null);
})
});
describe("convertBitmapToArray", () => {
it("empty", async () => {
const positionBundle = buildPositionBundleData(occupiedEmpty);
const result = PositionBundleUtil.convertBitmapToArray(positionBundle);
assert.equal(result.length, POSITION_BUNDLE_SIZE);
assert.ok(result.every((occupied) => !occupied));
});
it("some bundle indexes are occupied", async () => {
const positionBundle = buildPositionBundleData(occupiedPartial);
const result = PositionBundleUtil.convertBitmapToArray(positionBundle);
assert.equal(result.length, POSITION_BUNDLE_SIZE);
assert.ok(result.every((occupied, i) => occupied === occupiedPartial.includes(i)));
});
it("full", async () => {
const positionBundle = buildPositionBundleData(occupiedFull);
const result = PositionBundleUtil.convertBitmapToArray(positionBundle);
assert.equal(result.length, POSITION_BUNDLE_SIZE);
assert.ok(result.every((occupied) => occupied));
})
});
});

View File

@ -436,13 +436,6 @@ describe("whirlpool-impl", () => {
tickUpper: position.getUpperTickData(),
});
const rewardsQuote = collectRewardsQuote({
whirlpool: poolData,
position: positionData,
tickLower: position.getLowerTickData(),
tickUpper: position.getUpperTickData(),
});
let ataTx: TransactionBuilder | undefined;
let closeTx: TransactionBuilder;
if (txs.length === 1) {
@ -455,7 +448,19 @@ describe("whirlpool-impl", () => {
}
await ataTx?.buildAndExecute();
await closeTx.addSigner(otherWallet).buildAndExecute();
const signature = await closeTx.addSigner(otherWallet).buildAndExecute();
// To calculate the rewards that have accumulated up to the timing of the close,
// the block time at transaction execution is used.
const tx = await ctx.provider.connection.getTransaction(signature);
const closeTimestampInSeconds = new anchor.BN(tx!.blockTime!.toString());
const rewardsQuote = collectRewardsQuote({
whirlpool: poolData,
position: positionData,
tickLower: position.getLowerTickData(),
tickUpper: position.getUpperTickData(),
timeStampInSeconds: closeTimestampInSeconds,
});
assert.equal(
await getTokenBalance(ctx.provider, dWalletTokenAAccount),
@ -591,13 +596,6 @@ describe("whirlpool-impl", () => {
tickUpper: position.getUpperTickData(),
});
const rewardsQuote = collectRewardsQuote({
whirlpool: poolData,
position: positionData,
tickLower: position.getLowerTickData(),
tickUpper: position.getUpperTickData(),
});
const dWalletTokenBAccount = await deriveATA(otherWallet.publicKey, poolData.tokenMintB);
const rewardAccount0 = await deriveATA(otherWallet.publicKey, poolData.rewardInfos[0].mint);
const rewardAccount1 = await deriveATA(otherWallet.publicKey, poolData.rewardInfos[1].mint);
@ -626,7 +624,19 @@ describe("whirlpool-impl", () => {
const positionAccountBalance = await ctx.connection.getBalance(positionWithFees.publicKey);
await ataTx?.buildAndExecute();
await closeTx.addSigner(otherWallet).buildAndExecute();
const signature = await closeTx.addSigner(otherWallet).buildAndExecute();
// To calculate the rewards that have accumulated up to the timing of the close,
// the block time at transaction execution is used.
const tx = await ctx.provider.connection.getTransaction(signature);
const closeTimestampInSeconds = new anchor.BN(tx!.blockTime!.toString());
const rewardsQuote = collectRewardsQuote({
whirlpool: poolData,
position: positionData,
tickLower: position.getLowerTickData(),
tickUpper: position.getUpperTickData(),
timeStampInSeconds: closeTimestampInSeconds,
});
const otherWalletBalanceAfter = await ctx.connection.getBalance(otherWallet.publicKey);

View File

@ -1,4 +1,4 @@
import { AddressUtil, MathUtil, PDA } from "@orca-so/common-sdk";
import { deriveATA, AddressUtil, MathUtil, PDA } from "@orca-so/common-sdk";
import * as anchor from "@project-serum/anchor";
import { NATIVE_MINT, u64 } from "@solana/spl-token";
import { Keypair, PublicKey } from "@solana/web3.js";
@ -32,6 +32,7 @@ import {
generateDefaultInitFeeTierParams,
generateDefaultInitPoolParams,
generateDefaultInitTickArrayParams,
generateDefaultOpenBundledPositionParams,
generateDefaultOpenPositionParams,
TestConfigParams,
TestWhirlpoolsConfigKeypairs,
@ -883,3 +884,102 @@ export async function initTestPoolWithLiquidity(
feeTierParams,
};
}
export async function initializePositionBundleWithMetadata(
ctx: WhirlpoolContext,
owner: PublicKey = ctx.provider.wallet.publicKey,
funder?: Keypair
) {
const positionBundleMintKeypair = Keypair.generate();
const positionBundlePda = PDAUtil.getPositionBundle(ctx.program.programId, positionBundleMintKeypair.publicKey);
const positionBundleMetadataPda = PDAUtil.getPositionBundleMetadata(positionBundleMintKeypair.publicKey);
const positionBundleTokenAccount = await deriveATA(owner, positionBundleMintKeypair.publicKey);
const tx = toTx(ctx, WhirlpoolIx.initializePositionBundleWithMetadataIx(
ctx.program,
{
positionBundleMintKeypair,
positionBundlePda,
positionBundleMetadataPda,
owner,
positionBundleTokenAccount,
funder: !!funder ? funder.publicKey : owner,
},
));
if (funder) {
tx.addSigner(funder);
}
const txId = await tx.buildAndExecute();
return {
txId,
positionBundleMintKeypair,
positionBundlePda,
positionBundleMetadataPda,
positionBundleTokenAccount,
};
}
export async function initializePositionBundle(
ctx: WhirlpoolContext,
owner: PublicKey = ctx.provider.wallet.publicKey,
funder?: Keypair
) {
const positionBundleMintKeypair = Keypair.generate();
const positionBundlePda = PDAUtil.getPositionBundle(ctx.program.programId, positionBundleMintKeypair.publicKey);
const positionBundleTokenAccount = await deriveATA(owner, positionBundleMintKeypair.publicKey);
const tx = toTx(ctx, WhirlpoolIx.initializePositionBundleIx(
ctx.program,
{
positionBundleMintKeypair,
positionBundlePda,
owner,
positionBundleTokenAccount,
funder: !!funder ? funder.publicKey : owner,
},
));
if (funder) {
tx.addSigner(funder);
}
const txId = await tx.buildAndExecute();
return {
txId,
positionBundleMintKeypair,
positionBundlePda,
positionBundleTokenAccount,
};
}
export async function openBundledPosition(
ctx: WhirlpoolContext,
whirlpool: PublicKey,
positionBundleMint: PublicKey,
bundleIndex: number,
tickLowerIndex: number,
tickUpperIndex: number,
owner: PublicKey = ctx.provider.wallet.publicKey,
funder?: Keypair
) {
const { params } = await generateDefaultOpenBundledPositionParams(
ctx,
whirlpool,
positionBundleMint,
bundleIndex,
tickLowerIndex,
tickUpperIndex,
owner,
funder?.publicKey || owner
);
const tx = toTx(ctx, WhirlpoolIx.openBundledPositionIx(ctx.program, params));
if (funder) {
tx.addSigner(funder);
}
const txId = await tx.buildAndExecute();
return { txId, params };
}

View File

@ -16,6 +16,7 @@ import {
InitPoolParams,
InitTickArrayParams,
OpenPositionParams,
OpenBundledPositionParams,
PDAUtil,
PoolUtil,
PriceMath,
@ -254,3 +255,39 @@ export async function initPosition(
positionAddress: PDAUtil.getPosition(ctx.program.programId, positionMint),
};
}
export async function generateDefaultOpenBundledPositionParams(
context: WhirlpoolContext,
whirlpool: PublicKey,
positionBundleMint: PublicKey,
bundleIndex: number,
tickLowerIndex: number,
tickUpperIndex: number,
owner: PublicKey,
funder?: PublicKey
): Promise<{ params: Required<OpenBundledPositionParams> }> {
const bundledPositionPda = PDAUtil.getBundledPosition(context.program.programId, positionBundleMint, bundleIndex);
const positionBundle = PDAUtil.getPositionBundle(context.program.programId, positionBundleMint).publicKey;
const positionBundleTokenAccount = await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
positionBundleMint,
owner
);
const params: Required<OpenBundledPositionParams> = {
bundleIndex,
bundledPositionPda,
positionBundle,
positionBundleAuthority: owner,
funder: funder || owner,
positionBundleTokenAccount,
whirlpool: whirlpool,
tickLowerIndex,
tickUpperIndex,
};
return {
params,
};
}

View File

@ -2,9 +2,12 @@ import { ZERO } from "@orca-so/common-sdk";
import { web3 } from "@project-serum/anchor";
import { PublicKey, Keypair } from "@solana/web3.js";
import { BN } from "bn.js";
import invariant from "tiny-invariant";
import {
AccountFetcher,
PDAUtil,
PositionBundleData,
POSITION_BUNDLE_SIZE,
PriceMath,
TickArray,
TickArrayData,
@ -98,3 +101,16 @@ export async function getTickArrays(
};
});
}
export const buildPositionBundleData = (occupiedBundleIndexes: number[]): PositionBundleData => {
invariant(POSITION_BUNDLE_SIZE % 8 == 0, "POSITION_BUNDLE_SIZE should be multiple of 8");
const positionBundleMint = Keypair.generate().publicKey;
const positionBitmap: number[] = new Array(POSITION_BUNDLE_SIZE / 8).fill(0);
occupiedBundleIndexes.forEach((bundleIndex) => {
const index = Math.floor(bundleIndex / 8);
const offset = bundleIndex % 8;
positionBitmap[index] = positionBitmap[index] | (1 << offset);
});
return { positionBundleMint, positionBitmap };
};