Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SparseSwap (SDK) #162

Merged
merged 9 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions sdk/src/instructions/composites/swap-async.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { resolveOrCreateATAs, TransactionBuilder, ZERO } from "@orca-so/common-sdk";
import { PublicKey } from "@solana/web3.js";
import { SwapUtils, TickArrayUtil, Whirlpool, WhirlpoolContext } from "../..";
import { SwapUtils, Whirlpool, WhirlpoolContext } from "../..";
import { WhirlpoolAccountFetchOptions } from "../../network/public/fetcher";
import { SwapInput, swapIx } from "../swap-ix";
import { TokenExtensionUtil } from "../../utils/public/token-extension-util";
Expand All @@ -27,16 +27,8 @@ export async function swapAsync(
const { wallet, whirlpool, swapInput } = params;
const { aToB, amount } = swapInput;
const txBuilder = new TransactionBuilder(ctx.connection, ctx.wallet, ctx.txBuilderOpts);
const tickArrayAddresses = [swapInput.tickArray0, swapInput.tickArray1, swapInput.tickArray2];

let uninitializedArrays = await TickArrayUtil.getUninitializedArraysString(
tickArrayAddresses,
ctx.fetcher,
opts
);
if (uninitializedArrays) {
throw new Error(`TickArray addresses - [${uninitializedArrays}] need to be initialized.`);
}
// No need to check if TickArrays are initialized after SparseSwap implementation

const data = whirlpool.getData();
const [resolvedAtaA, resolvedAtaB] = await resolveOrCreateATAs(
Expand Down Expand Up @@ -72,7 +64,7 @@ export async function swapAsync(
wallet
);
return txBuilder.addInstruction(
!TokenExtensionUtil.isV2IxRequiredPool(tokenExtensionCtx)
(!TokenExtensionUtil.isV2IxRequiredPool(tokenExtensionCtx) && !params.swapInput.supplementalTickArrays)
? swapIx(ctx.program, baseParams)
: swapV2Ix(ctx.program, {
...baseParams,
Expand All @@ -90,6 +82,7 @@ export async function swapAsync(
baseParams.aToB ? baseParams.tokenOwnerAccountB : baseParams.tokenVaultB,
baseParams.aToB ? baseParams.whirlpool : baseParams.tokenAuthority,
),
supplementalTickArrays: params.swapInput.supplementalTickArrays,
})
);
}
10 changes: 1 addition & 9 deletions sdk/src/instructions/composites/swap-with-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
PDAUtil,
SubTradeRoute,
SwapUtils,
TickArrayUtil,
TradeRoute,
WhirlpoolContext,
twoHopSwapQuoteFromSwapQuotes,
Expand Down Expand Up @@ -111,14 +110,7 @@ export async function getSwapFromRoute(
}
}

let uninitializedArrays = await TickArrayUtil.getUninitializedArraysString(
requiredTickArrays,
ctx.fetcher,
opts
);
if (uninitializedArrays) {
throw new Error(`TickArray addresses - [${uninitializedArrays}] need to be initialized.`);
}
// No need to check if TickArrays are initialized after SparseSwap implementation

// Handle non-native mints only first
requiredAtas.delete(NATIVE_MINT.toBase58());
Expand Down
2 changes: 2 additions & 0 deletions sdk/src/instructions/swap-ix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export type SwapParams = SwapInput & {
* @param tickArray0 - PublicKey of the tick-array where the Whirlpool's currentTickIndex resides in
* @param tickArray1 - The next tick-array in the swap direction. If the swap will not reach the next tick-aray, input the same array as tickArray0.
* @param tickArray2 - The next tick-array in the swap direction after tickArray2. If the swap will not reach the next tick-aray, input the same array as tickArray1.
* @param supplementalTickArrays - (V2 only) Optional array of PublicKey for supplemental tick arrays. swap instruction will ignore this parameter.
*/
export type SwapInput = {
amount: BN;
Expand All @@ -51,6 +52,7 @@ export type SwapInput = {
tickArray0: PublicKey;
tickArray1: PublicKey;
tickArray2: PublicKey;
supplementalTickArrays?: PublicKey[];
};

/**
Expand Down
6 changes: 5 additions & 1 deletion sdk/src/instructions/two-hop-swap-ix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ export type TwoHopSwapParams = TwoHopSwapInput & {
* @param tickArrayTwo0 - PublicKey of the tick-array of swap-Two where the Whirlpool's currentTickIndex resides in
* @param tickArrayTwo1 - The next tick-array in the swap direction of swap-Two. If the swap will not reach the next tick-aray, input the same array as tickArray0.
* @param tickArrayTwo2 - The next tick-array in the swap direction after tickArray2 of swap-Two. If the swap will not reach the next tick-aray, input the same array as tickArray1.
*/
* @param supplementalTickArraysOne - (V2 only) Optional array of PublicKey for supplemental tick arrays of whirlpoolOne. twoHopSwap instruction will ignore this parameter.
* @param supplementalTickArraysTwo - (V2 only) Optional array of PublicKey for supplemental tick arrays of whirlpoolTwo. twoHopSwap instruction will ignore this parameter.
*/
export type TwoHopSwapInput = {
amount: BN;
otherAmountThreshold: BN;
Expand All @@ -73,6 +75,8 @@ export type TwoHopSwapInput = {
tickArrayTwo0: PublicKey;
tickArrayTwo1: PublicKey;
tickArrayTwo2: PublicKey;
supplementalTickArraysOne?: PublicKey[];
supplementalTickArraysTwo?: PublicKey[];
};

/**
Expand Down
2 changes: 0 additions & 2 deletions sdk/src/instructions/v2/swap-ix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import { RemainingAccountsBuilder, RemainingAccountsType, toSupplementalTickArra
* @param tokenProgramB - PublicKey for the token program for token B.
* @param oracle - PublicKey for the oracle account for this Whirlpool.
* @param tokenAuthority - authority to withdraw tokens from the input token account
* @param supplementalTickArrays - Optional array of PublicKey for supplemental tick arrays of this whirlpool.
*/
export type SwapV2Params = SwapInput & {
whirlpool: PublicKey;
Expand All @@ -40,7 +39,6 @@ export type SwapV2Params = SwapInput & {
tokenProgramB: PublicKey;
oracle: PublicKey;
tokenAuthority: PublicKey;
supplementalTickArrays?: PublicKey[];
};

/**
Expand Down
4 changes: 0 additions & 4 deletions sdk/src/instructions/v2/two-hop-swap-ix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ import { TwoHopSwapInput } from "../two-hop-swap-ix";
* @param oracleOne - PublicKey for the oracle account for this whirlpoolOne.
* @param oracleTwo - PublicKey for the oracle account for this whirlpoolTwo.
* @param tokenAuthority - authority to withdraw tokens from the input token account
* @param supplementalTickArraysOne - Optional array of PublicKey for supplemental tick arrays of whirlpoolOne.
* @param supplementalTickArraysTwo - Optional array of PublicKey for supplemental tick arrays of whirlpoolTwo.
* @param swapInput - Parameters in {@link TwoHopSwapInput}
*/
export type TwoHopSwapV2Params = TwoHopSwapInput & {
Expand All @@ -53,8 +51,6 @@ export type TwoHopSwapV2Params = TwoHopSwapInput & {
oracleOne: PublicKey;
oracleTwo: PublicKey;
tokenAuthority: PublicKey;
supplementalTickArraysOne?: PublicKey[];
supplementalTickArraysTwo?: PublicKey[];
};

/**
Expand Down
7 changes: 4 additions & 3 deletions sdk/src/prices/calculate-pool-prices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { swapQuoteWithParams } from "../quotes/public/swap-quote";
import { TickArray, WhirlpoolData } from "../types/public";
import { PoolUtil, PriceMath, SwapUtils } from "../utils/public";
import { NO_TOKEN_EXTENSION_CONTEXT } from "../utils/public/token-extension-util";
import { getTickArrayPublicKeysWithStartTickIndex } from "../utils/swap-utils";

function checkLiquidity(
pool: WhirlpoolData,
Expand Down Expand Up @@ -155,16 +156,16 @@ function getTickArrays(
config = defaultGetPricesConfig
): TickArray[] {
const { programId } = config;
const tickArrayPublicKeys = SwapUtils.getTickArrayPublicKeys(
const tickArrayAddresses = getTickArrayPublicKeysWithStartTickIndex(
pool.tickCurrentIndex,
pool.tickSpacing,
aToB,
programId,
address
);

return tickArrayPublicKeys.map((tickArrayPublicKey) => {
return { address: tickArrayPublicKey, data: tickArrayMap[tickArrayPublicKey.toBase58()] };
return tickArrayAddresses.map((a) => {
return { address: a.pubkey, startTickIndex: a.startTickIndex, data: tickArrayMap[a.pubkey.toBase58()] };
});
}

Expand Down
99 changes: 94 additions & 5 deletions sdk/src/quotes/public/swap-quote.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Address } from "@coral-xyz/anchor";
import { AddressUtil, MintWithTokenProgram, Percentage } from "@orca-so/common-sdk";
import { AddressUtil, Percentage } from "@orca-so/common-sdk";
import BN from "bn.js";
import invariant from "tiny-invariant";
import { SwapInput } from "../../instructions";
Expand All @@ -8,13 +8,27 @@ import {
WhirlpoolAccountFetchOptions,
WhirlpoolAccountFetcherInterface,
} from "../../network/public/fetcher";
import { TickArray, WhirlpoolData } from "../../types/public";
import { TICK_ARRAY_SIZE, TickArray, WhirlpoolData } from "../../types/public";
import { PoolUtil, SwapDirection } from "../../utils/public";
import { SwapUtils } from "../../utils/public/swap-utils";
import { Whirlpool } from "../../whirlpool-client";
import { simulateSwap } from "../swap/swap-quote-impl";
import { DevFeeSwapQuote } from "./dev-fee-swap-quote";
import { TokenExtensionContextForPool, TokenExtensionUtil } from "../../utils/public/token-extension-util";
import { PublicKey } from "@solana/web3.js";

/**
* An enum to specify when to use fallback tick array in a swap quote.
* @category Quotes
*/
export enum UseFallbackTickArray {
// Always try to include fallback tick array in the swap quote
Always = "Always",
// Never include fallback tick array in the swap quote
Never = "Never",
// Use fallback tick array only when tickCurrentIndex is the edge (last quoter) of the first tick array
Situational = "Situational",
}

/**
* @category Quotes
Expand All @@ -26,6 +40,8 @@ import { TokenExtensionContextForPool, TokenExtensionUtil } from "../../utils/pu
* @param amountSpecifiedIsInput - Specifies the token the parameter `amount`represents. If true, the amount represents
* the input token of the swap.
* @param tickArrays - An sequential array of tick-array objects in the direction of the trade to swap on
* @param tokenExtensionCtx - TokenExtensions info for the whirlpool
* @param fallbackTickArray - Optional. A reserve in case prices move in the opposite direction
*/
export type SwapQuoteParam = {
whirlpoolData: WhirlpoolData;
Expand All @@ -36,6 +52,7 @@ export type SwapQuoteParam = {
amountSpecifiedIsInput: boolean;
tickArrays: TickArray[];
tokenExtensionCtx: TokenExtensionContextForPool;
fallbackTickArray?: PublicKey;
};

/**
Expand Down Expand Up @@ -84,6 +101,7 @@ export type NormalSwapQuote = SwapInput & SwapEstimates;
* @param programId - PublicKey for the Whirlpool ProgramId
* @param cache - WhirlpoolAccountCacheInterface instance object to fetch solana accounts
* @param opts an {@link WhirlpoolAccountFetchOptions} object to define fetch and cache options when accessing on-chain accounts
* @param useFallbackTickArray - An enum to specify when to use fallback tick array in a swap quote.
* @returns a SwapQuote object with slippage adjusted SwapInput parameters & estimates on token amounts, fee & end whirlpool states.
*/
export async function swapQuoteByInputToken(
Expand All @@ -93,13 +111,15 @@ export async function swapQuoteByInputToken(
slippageTolerance: Percentage,
programId: Address,
fetcher: WhirlpoolAccountFetcherInterface,
opts?: WhirlpoolAccountFetchOptions
opts?: WhirlpoolAccountFetchOptions,
useFallbackTickArray: UseFallbackTickArray = UseFallbackTickArray.Never,
): Promise<SwapQuote> {
const params = await swapQuoteByToken(
whirlpool,
inputTokenMint,
tokenAmount,
true,
useFallbackTickArray,
programId,
fetcher,
opts
Expand All @@ -121,6 +141,7 @@ export async function swapQuoteByInputToken(
* @param programId - PublicKey for the Whirlpool ProgramId
* @param cache - WhirlpoolAccountCacheInterface instance to fetch solana accounts
* @param opts an {@link WhirlpoolAccountFetchOptions} object to define fetch and cache options when accessing on-chain accounts
* @param useFallbackTickArray - An enum to specify when to use fallback tick array in a swap quote.
* @returns a SwapQuote object with slippage adjusted SwapInput parameters & estimates on token amounts, fee & end whirlpool states.
*/
export async function swapQuoteByOutputToken(
Expand All @@ -130,13 +151,15 @@ export async function swapQuoteByOutputToken(
slippageTolerance: Percentage,
programId: Address,
fetcher: WhirlpoolAccountFetcherInterface,
opts?: WhirlpoolAccountFetchOptions
opts?: WhirlpoolAccountFetchOptions,
useFallbackTickArray: UseFallbackTickArray = UseFallbackTickArray.Never,
): Promise<SwapQuote> {
const params = await swapQuoteByToken(
whirlpool,
outputTokenMint,
tokenAmount,
false,
useFallbackTickArray,
programId,
fetcher,
opts
Expand All @@ -156,7 +179,20 @@ export function swapQuoteWithParams(
params: SwapQuoteParam,
slippageTolerance: Percentage
): SwapQuote {
const quote = simulateSwap(params);
const quote = simulateSwap({
...params,
tickArrays: SwapUtils.interpolateUninitializedTickArrays(PublicKey.default, params.tickArrays),
});

if (params.fallbackTickArray) {
if (quote.tickArray2.equals(quote.tickArray1)) {
// both V1 and V2 can use this fallback
quote.tickArray2 = params.fallbackTickArray;
} else {
// no obvious room for fallback, but V2 can use this field
quote.supplementalTickArrays = [params.fallbackTickArray];
}
}

const slippageAdjustedQuote: SwapQuote = {
...quote,
Expand All @@ -177,6 +213,7 @@ async function swapQuoteByToken(
inputTokenMint: Address,
tokenAmount: BN,
amountSpecifiedIsInput: boolean,
useFallbackTickArray: UseFallbackTickArray,
programId: Address,
fetcher: WhirlpoolAccountFetcherInterface,
opts?: WhirlpoolAccountFetchOptions
Expand All @@ -200,6 +237,14 @@ async function swapQuoteByToken(
opts
);

const fallbackTickArray = getFallbackTickArray(
useFallbackTickArray,
tickArrays,
aToB,
whirlpool,
programId,
);

const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext(fetcher, whirlpoolData, IGNORE_CACHE);

return {
Expand All @@ -211,5 +256,49 @@ async function swapQuoteByToken(
otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(amountSpecifiedIsInput),
tickArrays,
tokenExtensionCtx,
fallbackTickArray,
};
}

function getFallbackTickArray(
useFallbackTickArray: UseFallbackTickArray,
tickArrays: TickArray[],
aToB: boolean,
whirlpool: Whirlpool,
programId: Address,
): PublicKey | undefined {
if (useFallbackTickArray === UseFallbackTickArray.Never) {
return undefined;
}

const fallbackTickArray = SwapUtils.getFallbackTickArrayPublicKey(
tickArrays,
whirlpool.getData().tickSpacing,
aToB,
AddressUtil.toPubKey(programId),
whirlpool.getAddress(),
);

if (useFallbackTickArray === UseFallbackTickArray.Always || !fallbackTickArray) {
return fallbackTickArray;
}

invariant(
useFallbackTickArray === UseFallbackTickArray.Situational,
`Unexpected UseFallbackTickArray value: ${useFallbackTickArray}`
);

const ticksInArray = whirlpool.getData().tickSpacing * TICK_ARRAY_SIZE;
const tickCurrentIndex = whirlpool.getData().tickCurrentIndex;
if (aToB) {
// A to B (direction is right to left): [ ta2 ][ ta1 ][ ta0 ===]
// if tickCurrentIndex is within the rightmost quarter of ta0, use fallbackTickArray
const threshold = tickArrays[0].startTickIndex + ticksInArray / 4 * 3;
return tickCurrentIndex >= threshold ? fallbackTickArray : undefined;
} else {
// B to A (direction is left to right): [=== ta0 ][ ta1 ][ ta2 ]
// if tickCurrentIndex is within the leftmost quarter of ta0, use fallbackTickArray
const threshold = tickArrays[0].startTickIndex + ticksInArray / 4;
return tickCurrentIndex <= threshold ? fallbackTickArray : undefined;
}
}
2 changes: 2 additions & 0 deletions sdk/src/quotes/public/two-hop-swap-quote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export function twoHopSwapQuoteFromSwapQuotes(
tickArrayTwo0: swapQuoteTwo.tickArray0,
tickArrayTwo1: swapQuoteTwo.tickArray1,
tickArrayTwo2: swapQuoteTwo.tickArray2,
supplementalTickArraysOne: swapQuoteOne.supplementalTickArrays,
supplementalTickArraysTwo: swapQuoteTwo.supplementalTickArrays,
swapOneEstimates: { ...swapQuoteOne },
swapTwoEstimates: { ...swapQuoteTwo },
};
Expand Down
1 change: 1 addition & 0 deletions sdk/src/quotes/swap/tick-array-sequence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export class TickArraySequence {
}
this.sequence.push({
address: tickArray.address,
startTickIndex: tickArray.data.startTickIndex,
data: tickArray.data,
});
}
Expand Down
1 change: 1 addition & 0 deletions sdk/src/types/public/client-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@ export type WhirlpoolRewardInfo = WhirlpoolRewardInfoData & {
*/
export type TickArray = {
address: PublicKey;
startTickIndex: number;
data: TickArrayData | null;
};
Loading