Add collectFeesAndRewardsForPositions to allow easy fee/reward collection (#57)
- Add collectFeesAndRewardsForPositions to WhirlpoolClient - Add collectAllForPositionAddressesTxns to WhirlpoolIx
This commit is contained in:
parent
8a7ed57bc5
commit
3c7b90c147
|
@ -3,8 +3,7 @@
|
|||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@project-serum/anchor": "~0.25.0",
|
||||
"@solana/spl-token": "^0.1.8",
|
||||
"@orca-so/whirlpool-client-sdk": "0.0.7"
|
||||
"@solana/spl-token": "^0.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^9.0.0",
|
||||
|
|
|
@ -7,15 +7,15 @@
|
|||
"types": "dist/index.d.ts",
|
||||
"dependencies": {
|
||||
"@metaplex-foundation/mpl-token-metadata": "1.2.5",
|
||||
"@orca-so/common-sdk": "~0.1.1",
|
||||
"@solana/web3.js": "1.66.0",
|
||||
"@orca-so/common-sdk": "^0.1.4",
|
||||
"@project-serum/anchor": "~0.25.0",
|
||||
"@solana/spl-token": "^0.1.8",
|
||||
"decimal.js": "10.3.1",
|
||||
"decimal.js": "^10.3.1",
|
||||
"tiny-invariant": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bn.js": "~5.1.0",
|
||||
"@types/decimal.js": "^7.4.0",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.26.0",
|
||||
|
|
|
@ -5,27 +5,21 @@ import {
|
|||
TransactionBuilder,
|
||||
} from "@orca-so/common-sdk";
|
||||
import { Address } from "@project-serum/anchor";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import { WhirlpoolContext } from "../context";
|
||||
import {
|
||||
IncreaseLiquidityInput,
|
||||
DecreaseLiquidityInput,
|
||||
increaseLiquidityIx,
|
||||
decreaseLiquidityIx,
|
||||
IncreaseLiquidityInput,
|
||||
increaseLiquidityIx,
|
||||
} from "../instructions";
|
||||
import { PositionData } from "../types/public";
|
||||
import { PDAUtil, TickUtil } from "../utils/public";
|
||||
import { Position } from "../whirlpool-client";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import { AccountFetcher } from "../network/public";
|
||||
import { PDAUtil, TickUtil, toTx } from "../utils/public";
|
||||
|
||||
export class PositionImpl implements Position {
|
||||
private data: PositionData;
|
||||
constructor(
|
||||
readonly ctx: WhirlpoolContext,
|
||||
readonly fetcher: AccountFetcher,
|
||||
readonly address: PublicKey,
|
||||
data: PositionData
|
||||
) {
|
||||
constructor(readonly ctx: WhirlpoolContext, readonly address: PublicKey, data: PositionData) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
|
@ -57,7 +51,7 @@ export class PositionImpl implements Position {
|
|||
: this.ctx.wallet.publicKey;
|
||||
const ataPayerKey = ataPayer ? AddressUtil.toPubKey(ataPayer) : this.ctx.wallet.publicKey;
|
||||
|
||||
const whirlpool = await this.fetcher.getPool(this.data.whirlpool, true);
|
||||
const whirlpool = await this.ctx.fetcher.getPool(this.data.whirlpool, true);
|
||||
if (!whirlpool) {
|
||||
throw new Error("Unable to fetch whirlpool for this position.");
|
||||
}
|
||||
|
@ -78,7 +72,7 @@ export class PositionImpl implements Position {
|
|||
{ tokenMint: whirlpool.tokenMintA, wrappedSolAmountIn: liquidityInput.tokenMaxA },
|
||||
{ tokenMint: whirlpool.tokenMintB, wrappedSolAmountIn: liquidityInput.tokenMaxB },
|
||||
],
|
||||
() => this.fetcher.getAccountRentExempt(),
|
||||
() => this.ctx.fetcher.getAccountRentExempt(),
|
||||
ataPayerKey
|
||||
);
|
||||
const { address: ataAddrA, ...tokenOwnerAccountAIx } = ataA!;
|
||||
|
@ -132,7 +126,7 @@ export class PositionImpl implements Position {
|
|||
? AddressUtil.toPubKey(positionWallet)
|
||||
: this.ctx.wallet.publicKey;
|
||||
const ataPayerKey = ataPayer ? AddressUtil.toPubKey(ataPayer) : this.ctx.wallet.publicKey;
|
||||
const whirlpool = await this.fetcher.getPool(this.data.whirlpool, true);
|
||||
const whirlpool = await this.ctx.fetcher.getPool(this.data.whirlpool, true);
|
||||
|
||||
if (!whirlpool) {
|
||||
throw new Error("Unable to fetch whirlpool for this position.");
|
||||
|
@ -150,7 +144,7 @@ export class PositionImpl implements Position {
|
|||
this.ctx.connection,
|
||||
sourceWalletKey,
|
||||
[{ tokenMint: whirlpool.tokenMintA }, { tokenMint: whirlpool.tokenMintB }],
|
||||
() => this.fetcher.getAccountRentExempt(),
|
||||
() => this.ctx.fetcher.getAccountRentExempt(),
|
||||
ataPayerKey
|
||||
);
|
||||
const { address: ataAddrA, ...tokenOwnerAccountAIx } = ataA!;
|
||||
|
@ -190,7 +184,7 @@ export class PositionImpl implements Position {
|
|||
}
|
||||
|
||||
private async refresh() {
|
||||
const account = await this.fetcher.getPosition(this.address, true);
|
||||
const account = await this.ctx.fetcher.getPosition(this.address, true);
|
||||
if (!!account) {
|
||||
this.data = account;
|
||||
}
|
||||
|
|
|
@ -4,11 +4,12 @@ import { Keypair, PublicKey } from "@solana/web3.js";
|
|||
import invariant from "tiny-invariant";
|
||||
import { WhirlpoolContext } from "../context";
|
||||
import { initTickArrayIx } from "../instructions";
|
||||
import { collectAllForPositionAddressesTxns } from "../instructions/composites";
|
||||
import { WhirlpoolIx } from "../ix";
|
||||
import { AccountFetcher } from "../network/public";
|
||||
import { WhirlpoolData } from "../types/public";
|
||||
import { PDAUtil, PoolUtil, PriceMath, TickArrayUtil, TickUtil } from "../utils/public";
|
||||
import { WhirlpoolClient, Whirlpool, Position } from "../whirlpool-client";
|
||||
import { PDAUtil, PoolUtil, PriceMath, TickUtil } from "../utils/public";
|
||||
import { Position, Whirlpool, WhirlpoolClient } from "../whirlpool-client";
|
||||
import { PositionImpl } from "./position-impl";
|
||||
import { getRewardInfos, getTokenMintInfos, getTokenVaultAccountInfos } from "./util";
|
||||
import { WhirlpoolImpl } from "./whirlpool-impl";
|
||||
|
@ -34,7 +35,6 @@ export class WhirlpoolClientImpl implements WhirlpoolClient {
|
|||
const rewardInfos = await getRewardInfos(this.ctx.fetcher, account, refresh);
|
||||
return new WhirlpoolImpl(
|
||||
this.ctx,
|
||||
this.ctx.fetcher,
|
||||
AddressUtil.toPubKey(poolAddress),
|
||||
tokenInfos[0],
|
||||
tokenInfos[1],
|
||||
|
@ -78,7 +78,6 @@ export class WhirlpoolClientImpl implements WhirlpoolClient {
|
|||
whirlpools.push(
|
||||
new WhirlpoolImpl(
|
||||
this.ctx,
|
||||
this.ctx.fetcher,
|
||||
AddressUtil.toPubKey(poolAddress),
|
||||
tokenInfos[0],
|
||||
tokenInfos[1],
|
||||
|
@ -97,12 +96,7 @@ export class WhirlpoolClientImpl implements WhirlpoolClient {
|
|||
if (!account) {
|
||||
throw new Error(`Unable to fetch Position at address at ${positionAddress}`);
|
||||
}
|
||||
return new PositionImpl(
|
||||
this.ctx,
|
||||
this.ctx.fetcher,
|
||||
AddressUtil.toPubKey(positionAddress),
|
||||
account
|
||||
);
|
||||
return new PositionImpl(this.ctx, AddressUtil.toPubKey(positionAddress), account);
|
||||
}
|
||||
|
||||
public async getPositions(
|
||||
|
@ -116,15 +110,7 @@ export class WhirlpoolClientImpl implements WhirlpoolClient {
|
|||
return [address, null];
|
||||
}
|
||||
|
||||
return [
|
||||
address,
|
||||
new PositionImpl(
|
||||
this.ctx,
|
||||
this.ctx.fetcher,
|
||||
AddressUtil.toPubKey(address),
|
||||
positionAccount
|
||||
),
|
||||
];
|
||||
return [address, new PositionImpl(this.ctx, AddressUtil.toPubKey(address), positionAccount)];
|
||||
});
|
||||
|
||||
return Object.fromEntries(results);
|
||||
|
@ -217,4 +203,22 @@ export class WhirlpoolClientImpl implements WhirlpoolClient {
|
|||
tx: txBuilder,
|
||||
};
|
||||
}
|
||||
|
||||
public async collectFeesAndRewardsForPositions(
|
||||
positionAddresses: Address[],
|
||||
refresh?: boolean | undefined
|
||||
): Promise<TransactionBuilder[]> {
|
||||
const walletKey = this.ctx.wallet.publicKey;
|
||||
return collectAllForPositionAddressesTxns(
|
||||
this.ctx,
|
||||
{
|
||||
positions: positionAddresses,
|
||||
receiver: walletKey,
|
||||
positionAuthority: walletKey,
|
||||
positionOwner: walletKey,
|
||||
payer: walletKey,
|
||||
},
|
||||
refresh
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
resolveOrCreateATAs,
|
||||
TokenUtil,
|
||||
TransactionBuilder,
|
||||
ZERO
|
||||
ZERO,
|
||||
} from "@orca-so/common-sdk";
|
||||
import { Address, BN, translateAddress } from "@project-serum/anchor";
|
||||
import { Keypair, PublicKey } from "@solana/web3.js";
|
||||
|
@ -21,9 +21,8 @@ import {
|
|||
openPositionIx,
|
||||
openPositionWithMetadataIx,
|
||||
SwapInput,
|
||||
swapIx
|
||||
swapIx,
|
||||
} from "../instructions";
|
||||
import { AccountFetcher } from "../network/public";
|
||||
import { decreaseLiquidityQuoteByLiquidityWithParams } from "../quotes/public";
|
||||
import { TokenAccountInfo, TokenInfo, WhirlpoolData, WhirlpoolRewardInfo } from "../types/public";
|
||||
import { PDAUtil, TickArrayUtil, TickUtil } from "../utils/public";
|
||||
|
@ -34,7 +33,6 @@ export class WhirlpoolImpl implements Whirlpool {
|
|||
private data: WhirlpoolData;
|
||||
constructor(
|
||||
readonly ctx: WhirlpoolContext,
|
||||
readonly fetcher: AccountFetcher,
|
||||
readonly address: PublicKey,
|
||||
readonly tokenAInfo: TokenInfo,
|
||||
readonly tokenBInfo: TokenInfo,
|
||||
|
@ -121,7 +119,7 @@ export class WhirlpoolImpl implements Whirlpool {
|
|||
this.ctx.program.programId,
|
||||
this.address,
|
||||
this.data.tickSpacing,
|
||||
this.fetcher,
|
||||
this.ctx.fetcher,
|
||||
refresh
|
||||
);
|
||||
|
||||
|
@ -229,7 +227,7 @@ export class WhirlpoolImpl implements Whirlpool {
|
|||
|
||||
invariant(liquidity.gt(new BN(0)), "liquidity must be greater than zero");
|
||||
|
||||
const whirlpool = await this.fetcher.getPool(this.address, false);
|
||||
const whirlpool = await this.ctx.fetcher.getPool(this.address, false);
|
||||
if (!whirlpool) {
|
||||
throw new Error(`Whirlpool not found: ${translateAddress(this.address).toBase58()}`);
|
||||
}
|
||||
|
@ -279,7 +277,7 @@ export class WhirlpoolImpl implements Whirlpool {
|
|||
{ tokenMint: whirlpool.tokenMintA, wrappedSolAmountIn: tokenMaxA },
|
||||
{ tokenMint: whirlpool.tokenMintB, wrappedSolAmountIn: tokenMaxB },
|
||||
],
|
||||
() => this.fetcher.getAccountRentExempt(),
|
||||
() => this.ctx.fetcher.getAccountRentExempt(),
|
||||
funder
|
||||
);
|
||||
const { address: tokenOwnerAccountA, ...tokenOwnerAccountAIx } = ataA;
|
||||
|
@ -331,7 +329,7 @@ export class WhirlpoolImpl implements Whirlpool {
|
|||
positionWallet: PublicKey,
|
||||
payerKey: PublicKey
|
||||
): Promise<TransactionBuilder> {
|
||||
const position = await this.fetcher.getPosition(positionAddress, true);
|
||||
const position = await this.ctx.fetcher.getPosition(positionAddress, true);
|
||||
if (!position) {
|
||||
throw new Error(`Position not found: ${positionAddress.toBase58()}`);
|
||||
}
|
||||
|
@ -367,7 +365,7 @@ export class WhirlpoolImpl implements Whirlpool {
|
|||
this.ctx.connection,
|
||||
destinationWallet,
|
||||
[{ tokenMint: whirlpool.tokenMintA }, { tokenMint: whirlpool.tokenMintB }],
|
||||
() => this.fetcher.getAccountRentExempt(),
|
||||
() => this.ctx.fetcher.getAccountRentExempt(),
|
||||
payerKey
|
||||
);
|
||||
|
||||
|
@ -460,7 +458,7 @@ export class WhirlpoolImpl implements Whirlpool {
|
|||
{ tokenMint: whirlpool.tokenMintA, wrappedSolAmountIn: aToB ? amount : ZERO },
|
||||
{ tokenMint: whirlpool.tokenMintB, wrappedSolAmountIn: !aToB ? amount : ZERO },
|
||||
],
|
||||
() => this.fetcher.getAccountRentExempt()
|
||||
() => this.ctx.fetcher.getAccountRentExempt()
|
||||
);
|
||||
|
||||
const { address: tokenOwnerAccountA, ...tokenOwnerAccountAIx } = ataA;
|
||||
|
@ -488,11 +486,11 @@ export class WhirlpoolImpl implements Whirlpool {
|
|||
}
|
||||
|
||||
private async refresh() {
|
||||
const account = await this.fetcher.getPool(this.address, true);
|
||||
const account = await this.ctx.fetcher.getPool(this.address, true);
|
||||
if (!!account) {
|
||||
const rewardInfos = await getRewardInfos(this.fetcher, account, true);
|
||||
const rewardInfos = await getRewardInfos(this.ctx.fetcher, account, true);
|
||||
const [tokenVaultAInfo, tokenVaultBInfo] = await getTokenVaultAccountInfos(
|
||||
this.fetcher,
|
||||
this.ctx.fetcher,
|
||||
account,
|
||||
true
|
||||
);
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import Decimal from "decimal.js";
|
||||
|
||||
export * from "./context";
|
||||
export * from "./types/public";
|
||||
export * from "./utils/public";
|
||||
export * from "./impl/position-impl";
|
||||
export * from "./ix";
|
||||
export * from "./network/public";
|
||||
export * from "./quotes/public";
|
||||
export * from "./ix";
|
||||
export * from "./whirlpool-client";
|
||||
|
||||
export * from "./types/public";
|
||||
export * from "./types/public/anchor-types";
|
||||
export * from "./utils/public";
|
||||
export * from "./whirlpool-client";
|
||||
|
||||
// Global rules for Decimals
|
||||
// - 40 digits of precision for the largest number
|
||||
|
|
|
@ -0,0 +1,292 @@
|
|||
import { AddressUtil, Instruction, TokenUtil, TransactionBuilder, ZERO } from "@orca-so/common-sdk";
|
||||
import { createWSOLAccountInstructions } from "@orca-so/common-sdk/dist/helpers/token-instructions";
|
||||
import { Address } from "@project-serum/anchor";
|
||||
import { NATIVE_MINT } from "@solana/spl-token";
|
||||
import { PACKET_DATA_SIZE, PublicKey } from "@solana/web3.js";
|
||||
import { updateFeesAndRewardsIx } from "..";
|
||||
import { WhirlpoolContext } from "../..";
|
||||
import { PositionImpl } from "../../impl/position-impl";
|
||||
import { WhirlpoolIx } from "../../ix";
|
||||
import { WhirlpoolData } from "../../types/public";
|
||||
import { PDAUtil, PoolUtil, TickUtil } from "../../utils/public";
|
||||
import { getAssociatedTokenAddressSync } from "../../utils/spl-token-utils";
|
||||
import { convertListToMap } from "../../utils/txn-utils";
|
||||
import { Position } from "../../whirlpool-client";
|
||||
import { resolveAtaForWhirlpoolsIxs } from "./resolve-atas-ix";
|
||||
|
||||
/**
|
||||
* Parameters to collect all fees and rewards from a list of positions.
|
||||
*
|
||||
* @category Instruction Types
|
||||
* @param positionAddrs - An array of Whirlpool position addresses.
|
||||
* @param receiver - The destination wallet that collected fees & reward will be sent to. Defaults to ctx.wallet key.
|
||||
* @param positionOwner - The wallet key that contains the position token. Defaults to ctx.wallet key.
|
||||
* @param positionAuthority - The authority key that can authorize operation on the position. Defaults to ctx.wallet key.
|
||||
* @param payer - The key that will pay for the initialization of ATA token accounts. Defaults to ctx.wallet key.
|
||||
*/
|
||||
export type CollectAllPositionAddressParams = {
|
||||
positions: Address[];
|
||||
} & CollectAllParams;
|
||||
|
||||
/**
|
||||
* Parameters to collect all fees and rewards from a list of positions.
|
||||
*
|
||||
* @category Instruction Types
|
||||
* @param positions - An array of Whirlpool positions.
|
||||
* @param receiver - The destination wallet that collected fees & reward will be sent to. Defaults to ctx.wallet key.
|
||||
* @param positionOwner - The wallet key that contains the position token. Defaults to ctx.wallet key.
|
||||
* @param positionAuthority - The authority key that can authorize operation on the position. Defaults to ctx.wallet key.
|
||||
* @param payer - The key that will pay for the initialization of ATA token accounts. Defaults to ctx.wallet key.
|
||||
*/
|
||||
export type CollectAllPositionParams = {
|
||||
positions: Position[];
|
||||
} & CollectAllParams;
|
||||
|
||||
type CollectAllParams = {
|
||||
receiver?: PublicKey;
|
||||
positionOwner?: PublicKey;
|
||||
positionAuthority?: PublicKey;
|
||||
payer?: PublicKey;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a set of transactions to collect fees and rewards for a set of Whirlpool Positions.
|
||||
*
|
||||
* @category Instructions
|
||||
* @experimental
|
||||
* @param ctx - WhirlpoolContext object for the current environment.
|
||||
* @param params - CollectAllPositionAddressParams object
|
||||
* @param refresh - if true, will always fetch for the latest on-chain data.
|
||||
* @returns A set of transaction-builders to resolve ATA for affliated tokens, collect fee & rewards for all positions.
|
||||
* The first transaction should always be processed as it contains all the resolve ATA instructions to receive tokens.
|
||||
*/
|
||||
export async function collectAllForPositionAddressesTxns(
|
||||
ctx: WhirlpoolContext,
|
||||
params: CollectAllPositionAddressParams,
|
||||
refresh = false
|
||||
): Promise<TransactionBuilder[]> {
|
||||
const { positions, ...rest } = params;
|
||||
const posDatas = await ctx.fetcher.listPositions(positions, refresh);
|
||||
const positionsObjs = posDatas.reduce<Position[]>((accu, curr, index) => {
|
||||
if (curr) {
|
||||
accu.push(new PositionImpl(ctx, AddressUtil.toPubKey(positions[index]), curr));
|
||||
}
|
||||
return accu;
|
||||
}, []);
|
||||
return collectAllForPositionsTxns(ctx, { positions: positionsObjs, ...rest });
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a set of transactions to collect fees and rewards for a set of Whirlpool Positions.
|
||||
*
|
||||
* @experimental
|
||||
* @param ctx - WhirlpoolContext object for the current environment.
|
||||
* @param params - CollectAllPositionParams object
|
||||
* @returns A set of transaction-builders to resolve ATA for affliated tokens, collect fee & rewards for all positions.
|
||||
*/
|
||||
export async function collectAllForPositionsTxns(
|
||||
ctx: WhirlpoolContext,
|
||||
params: CollectAllPositionParams
|
||||
): Promise<TransactionBuilder[]> {
|
||||
const { positions, receiver, positionAuthority, positionOwner, payer } = params;
|
||||
const receiverKey = receiver ?? ctx.wallet.publicKey;
|
||||
const positionAuthorityKey = positionAuthority ?? ctx.wallet.publicKey;
|
||||
const positionOwnerKey = positionOwner ?? ctx.wallet.publicKey;
|
||||
const payerKey = payer ?? ctx.wallet.publicKey;
|
||||
|
||||
if (positions.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const whirlpoolAddrs = positions.map((pos) => pos.getData().whirlpool.toBase58());
|
||||
const whirlpoolDatas = await ctx.fetcher.listPools(whirlpoolAddrs, false);
|
||||
const whirlpools = convertListToMap(whirlpoolDatas, whirlpoolAddrs);
|
||||
|
||||
// TODO: Payer is not configurable here. Forced to use wallet
|
||||
const { ataTokenAddresses: affliatedTokenAtaMap, resolveAtaIxs } =
|
||||
await resolveAtaForWhirlpoolsIxs(ctx, {
|
||||
whirlpools: whirlpoolDatas,
|
||||
receiver: receiverKey,
|
||||
payer: payerKey,
|
||||
});
|
||||
|
||||
const latestBlockhash = await ctx.connection.getLatestBlockhash("singleGossip");
|
||||
const accountExemption = await ctx.fetcher.getAccountRentExempt();
|
||||
const txBuilders: TransactionBuilder[] = [];
|
||||
|
||||
let pendingTxBuilder = new TransactionBuilder(ctx.connection, ctx.wallet).addInstructions(
|
||||
resolveAtaIxs
|
||||
);
|
||||
let pendingTxBuilderTxSize = await pendingTxBuilder.txnSize({ latestBlockhash });
|
||||
|
||||
let posIndex = 0;
|
||||
let reattempt = false;
|
||||
|
||||
while (posIndex < positions.length) {
|
||||
const position = positions[posIndex];
|
||||
let positionTxBuilder = new TransactionBuilder(ctx.connection, ctx.wallet);
|
||||
const { whirlpool: whirlpoolKey, positionMint } = position.getData();
|
||||
const whirlpool = whirlpools[whirlpoolKey.toBase58()];
|
||||
|
||||
if (!whirlpool) {
|
||||
throw new Error(
|
||||
`Unable to process positionMint ${positionMint} - unable to derive whirlpool ${whirlpoolKey.toBase58()}`
|
||||
);
|
||||
}
|
||||
const posHandlesNativeMint =
|
||||
TokenUtil.isNativeMint(whirlpool.tokenMintA) || TokenUtil.isNativeMint(whirlpool.tokenMintB);
|
||||
const txBuilderHasNativeMint = !!affliatedTokenAtaMap[NATIVE_MINT.toBase58()];
|
||||
|
||||
// Add NATIVE_MINT token account creation to this transaction if position requires NATIVE_MINT handling.
|
||||
if (posHandlesNativeMint && !txBuilderHasNativeMint) {
|
||||
addNativeMintHandlingIx(
|
||||
positionTxBuilder,
|
||||
affliatedTokenAtaMap,
|
||||
receiverKey,
|
||||
accountExemption
|
||||
);
|
||||
}
|
||||
// Build position instructions
|
||||
const collectIxForPosition = constructCollectPositionIx(
|
||||
ctx,
|
||||
position,
|
||||
whirlpools,
|
||||
positionOwnerKey,
|
||||
positionAuthorityKey,
|
||||
affliatedTokenAtaMap
|
||||
);
|
||||
positionTxBuilder.addInstructions(collectIxForPosition);
|
||||
|
||||
// Attempt to push the new instructions into the pending builder
|
||||
// Iterate to the next position if possible
|
||||
// Create a builder and reattempt if the current one is full.
|
||||
const incrementTxSize = await positionTxBuilder.txnSize({ latestBlockhash });
|
||||
if (pendingTxBuilderTxSize + incrementTxSize < PACKET_DATA_SIZE) {
|
||||
pendingTxBuilder.addInstruction(positionTxBuilder.compressIx(false));
|
||||
pendingTxBuilderTxSize = pendingTxBuilderTxSize + incrementTxSize;
|
||||
posIndex += 1;
|
||||
reattempt = false;
|
||||
} else {
|
||||
if (reattempt) {
|
||||
throw new Error(
|
||||
`Unable to fit collection ix for ${position.getAddress().toBase58()} in a Transaction.`
|
||||
);
|
||||
}
|
||||
|
||||
txBuilders.push(pendingTxBuilder);
|
||||
delete affliatedTokenAtaMap[NATIVE_MINT.toBase58()];
|
||||
pendingTxBuilder = new TransactionBuilder(ctx.connection, ctx.provider.wallet);
|
||||
pendingTxBuilderTxSize = 0;
|
||||
reattempt = true;
|
||||
}
|
||||
}
|
||||
|
||||
txBuilders.push(pendingTxBuilder);
|
||||
return txBuilders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper methods.
|
||||
*/
|
||||
function addNativeMintHandlingIx(
|
||||
txBuilder: TransactionBuilder,
|
||||
affliatedTokenAtaMap: Record<string, PublicKey>,
|
||||
destinationWallet: PublicKey,
|
||||
accountExemption: number
|
||||
) {
|
||||
let { address: wSOLAta, ...resolveWSolIx } = createWSOLAccountInstructions(
|
||||
destinationWallet,
|
||||
ZERO,
|
||||
accountExemption
|
||||
);
|
||||
affliatedTokenAtaMap[NATIVE_MINT.toBase58()] = wSOLAta;
|
||||
txBuilder.prependInstruction(resolveWSolIx);
|
||||
}
|
||||
|
||||
// TODO: Once individual collect ix for positions is implemented, maybe migrate over if it can take custom ATA?
|
||||
const constructCollectPositionIx = (
|
||||
ctx: WhirlpoolContext,
|
||||
position: Position,
|
||||
whirlpools: Record<string, WhirlpoolData | null>,
|
||||
positionOwner: PublicKey,
|
||||
positionAuthority: PublicKey,
|
||||
affliatedTokenAtaMap: Record<string, PublicKey>
|
||||
) => {
|
||||
const ixForPosition: Instruction[] = [];
|
||||
const {
|
||||
whirlpool: whirlpoolKey,
|
||||
liquidity,
|
||||
tickLowerIndex,
|
||||
tickUpperIndex,
|
||||
positionMint,
|
||||
rewardInfos: positionRewardInfos,
|
||||
} = position.getData();
|
||||
const positionKey = AddressUtil.toPubKey(position.getAddress());
|
||||
const whirlpool = whirlpools[whirlpoolKey.toBase58()];
|
||||
|
||||
if (!whirlpool) {
|
||||
throw new Error(
|
||||
`Unable to process positionMint ${positionMint} - unable to derive whirlpool ${whirlpoolKey.toBase58()}`
|
||||
);
|
||||
}
|
||||
const { tickSpacing } = whirlpool;
|
||||
|
||||
// Update fee and reward values if necessary
|
||||
if (!liquidity.eq(ZERO)) {
|
||||
ixForPosition.push(
|
||||
updateFeesAndRewardsIx(ctx.program, {
|
||||
position: positionKey,
|
||||
whirlpool: whirlpoolKey,
|
||||
tickArrayLower: PDAUtil.getTickArray(
|
||||
ctx.program.programId,
|
||||
whirlpoolKey,
|
||||
TickUtil.getStartTickIndex(tickLowerIndex, tickSpacing)
|
||||
).publicKey,
|
||||
tickArrayUpper: PDAUtil.getTickArray(
|
||||
ctx.program.programId,
|
||||
whirlpoolKey,
|
||||
TickUtil.getStartTickIndex(tickUpperIndex, tickSpacing)
|
||||
).publicKey,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Collect Fee
|
||||
const positionTokenAccount = getAssociatedTokenAddressSync(
|
||||
positionMint.toBase58(),
|
||||
positionOwner.toBase58()
|
||||
);
|
||||
ixForPosition.push(
|
||||
WhirlpoolIx.collectFeesIx(ctx.program, {
|
||||
whirlpool: whirlpoolKey,
|
||||
position: positionKey,
|
||||
positionAuthority,
|
||||
positionTokenAccount,
|
||||
tokenOwnerAccountA: affliatedTokenAtaMap[whirlpool.tokenMintA.toBase58()],
|
||||
tokenOwnerAccountB: affliatedTokenAtaMap[whirlpool.tokenMintB.toBase58()],
|
||||
tokenVaultA: whirlpool.tokenVaultA,
|
||||
tokenVaultB: whirlpool.tokenVaultB,
|
||||
})
|
||||
);
|
||||
|
||||
// Collect Rewards
|
||||
// TODO: handle empty vault values?
|
||||
positionRewardInfos.forEach((_, index) => {
|
||||
const rewardInfo = whirlpool.rewardInfos[index];
|
||||
if (PoolUtil.isRewardInitialized(rewardInfo)) {
|
||||
ixForPosition.push(
|
||||
WhirlpoolIx.collectRewardIx(ctx.program, {
|
||||
whirlpool: whirlpoolKey,
|
||||
position: positionKey,
|
||||
positionAuthority,
|
||||
positionTokenAccount,
|
||||
rewardIndex: index,
|
||||
rewardOwnerAccount: affliatedTokenAtaMap[rewardInfo.mint.toBase58()],
|
||||
rewardVault: rewardInfo.vault,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return ixForPosition;
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./collect-all-txn";
|
||||
export * from "./resolve-atas-ix";
|
|
@ -0,0 +1,136 @@
|
|||
import { Instruction, TokenUtil } from "@orca-so/common-sdk";
|
||||
import { ASSOCIATED_TOKEN_PROGRAM_ID, Token, TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import { WhirlpoolContext } from "../..";
|
||||
import { WhirlpoolData } from "../../types/public";
|
||||
import { getAssociatedTokenAddressSync } from "../../utils/spl-token-utils";
|
||||
import { convertListToMap } from "../../utils/txn-utils";
|
||||
|
||||
/**
|
||||
* Parameters to resolve ATAs for affliated tokens in a list of Whirlpools
|
||||
*
|
||||
* @category Instruction Types
|
||||
* @param whirlpools - The list of WhirlpoolData to generate affliated tokens for.
|
||||
* @param destinationWallet - the wallet to generate ATAs against
|
||||
* @param payer - The payer address that would pay for the creation of ATA addresses
|
||||
*/
|
||||
export type ResolveAtaInstructionParams = {
|
||||
whirlpools: (WhirlpoolData | null)[];
|
||||
receiver?: PublicKey;
|
||||
payer?: PublicKey;
|
||||
};
|
||||
|
||||
/**
|
||||
* An interface of mapping between tokenMint & ATA & the instruction set to initialize them.
|
||||
*
|
||||
* @category Instruction Types
|
||||
* @param ataTokenAddresses - A record between the token mint & generated ATA addresses
|
||||
* @param resolveAtaIxs - An array of instructions to initialize all uninitialized ATA token accounts for the list above.
|
||||
*/
|
||||
export type ResolvedATAInstructionSet = {
|
||||
ataTokenAddresses: Record<string, PublicKey>;
|
||||
resolveAtaIxs: Instruction[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Build instructions to resolve ATAs (Associated Tokens Addresses) for affliated tokens in a list of Whirlpools.
|
||||
* Affliated tokens are tokens that are part of the trade pair or reward in a Whirlpool.
|
||||
*
|
||||
* SOL tokens does not use the ATA program and therefore not handled.
|
||||
*
|
||||
* @param ctx - WhirlpoolContext object for the current environment.
|
||||
|
||||
* @returns a ResolvedTokenAddressesIxSet containing the derived ATA addresses & ix set to initialize the accounts.
|
||||
*/
|
||||
export async function resolveAtaForWhirlpoolsIxs(
|
||||
ctx: WhirlpoolContext,
|
||||
params: ResolveAtaInstructionParams
|
||||
): Promise<ResolvedATAInstructionSet> {
|
||||
const { whirlpools, receiver, payer } = params;
|
||||
const receiverKey = receiver ?? ctx.wallet.publicKey;
|
||||
const payerKey = payer ?? ctx.wallet.publicKey;
|
||||
const { affliatedTokenAtaMap, affliatedTokensInfoMap } = await getAffliatedTokenAtas(
|
||||
ctx,
|
||||
whirlpools,
|
||||
receiverKey
|
||||
);
|
||||
|
||||
const tokensRequiringAtaResolve = Object.fromEntries(
|
||||
Object.entries(affliatedTokensInfoMap)
|
||||
.filter(([, account]) => !account)
|
||||
.map(([mint]) => [mint, affliatedTokenAtaMap[mint]])
|
||||
);
|
||||
|
||||
const resolveAtaIxs: Instruction[] = [];
|
||||
|
||||
Object.entries(tokensRequiringAtaResolve).forEach(([mint, ataKey]) => {
|
||||
const createAtaInstruction = Token.createAssociatedTokenAccountInstruction(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
new PublicKey(mint),
|
||||
ataKey,
|
||||
receiverKey,
|
||||
payerKey
|
||||
);
|
||||
resolveAtaIxs.push({
|
||||
instructions: [createAtaInstruction],
|
||||
cleanupInstructions: [],
|
||||
signers: [],
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
ataTokenAddresses: affliatedTokenAtaMap,
|
||||
resolveAtaIxs,
|
||||
};
|
||||
}
|
||||
|
||||
async function getAffliatedTokenAtas(
|
||||
ctx: WhirlpoolContext,
|
||||
whirlpoolDatas: (WhirlpoolData | null)[],
|
||||
wallet: PublicKey
|
||||
) {
|
||||
const affliatedTokens = Array.from(
|
||||
whirlpoolDatas.reduce<Set<string>>((accu, whirlpoolData) => {
|
||||
if (whirlpoolData) {
|
||||
const { tokenMintA, tokenMintB } = whirlpoolData;
|
||||
if (!TokenUtil.isNativeMint(tokenMintA)) {
|
||||
accu.add(tokenMintA.toBase58());
|
||||
}
|
||||
|
||||
if (!TokenUtil.isNativeMint(tokenMintB)) {
|
||||
accu.add(tokenMintB.toBase58());
|
||||
}
|
||||
|
||||
const rewardInfos = whirlpoolData.rewardInfos;
|
||||
rewardInfos.forEach((reward) => {
|
||||
if (!reward.mint.equals(PublicKey.default)) {
|
||||
accu.add(reward.mint.toBase58());
|
||||
}
|
||||
});
|
||||
}
|
||||
return accu;
|
||||
}, new Set<string>())
|
||||
);
|
||||
|
||||
const tokenMintInfoMap = convertListToMap(
|
||||
await ctx.fetcher.listMintInfos(affliatedTokens, false),
|
||||
affliatedTokens
|
||||
);
|
||||
|
||||
// Derive associated addresses for all affliated spl-tokens
|
||||
const affliatedTokenAtaMap = Object.fromEntries(
|
||||
Object.keys(tokenMintInfoMap).map((addr) => [
|
||||
addr,
|
||||
getAssociatedTokenAddressSync(addr, wallet.toBase58()),
|
||||
])
|
||||
);
|
||||
|
||||
return {
|
||||
affliatedTokenAtaMap: affliatedTokenAtaMap,
|
||||
affliatedTokensInfoMap: convertListToMap(
|
||||
await ctx.fetcher.listTokenInfos(Object.values(affliatedTokenAtaMap), false),
|
||||
Object.keys(affliatedTokenAtaMap)
|
||||
),
|
||||
};
|
||||
}
|
|
@ -2,6 +2,7 @@ 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 "./increase-liquidity-ix";
|
||||
export * from "./initialize-config-ix";
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { PDA } from "@orca-so/common-sdk";
|
||||
import { Program } from "@project-serum/anchor";
|
||||
import { WhirlpoolContext } from ".";
|
||||
import { Whirlpool } from "./artifacts/whirlpool";
|
||||
import * as ix from "./instructions";
|
||||
|
||||
|
@ -415,4 +416,20 @@ export class WhirlpoolIx {
|
|||
) {
|
||||
return ix.setRewardEmissionsSuperAuthorityIx(program, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* A set of transactions to collect all fees and rewards from a list of positions.
|
||||
*
|
||||
* @param ctx - WhirlpoolContext object for the current environment.
|
||||
* @param params - CollectAllPositionAddressParams object.
|
||||
* @param refresh - if true, will always fetch for the latest values on chain to compute.
|
||||
* @returns
|
||||
*/
|
||||
public static collectAllForPositionsTxns(
|
||||
ctx: WhirlpoolContext,
|
||||
params: ix.CollectAllPositionAddressParams,
|
||||
refresh: boolean
|
||||
) {
|
||||
return ix.collectAllForPositionAddressesTxns(ctx, params, refresh);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,9 @@ import { DecreaseLiquidityInput } from "../../instructions";
|
|||
import {
|
||||
adjustForSlippage,
|
||||
getTokenAFromLiquidity,
|
||||
getTokenBFromLiquidity, PositionStatus, PositionUtil
|
||||
getTokenBFromLiquidity,
|
||||
PositionStatus,
|
||||
PositionUtil,
|
||||
} from "../../utils/position-util";
|
||||
import { PriceMath, TickUtil } from "../../utils/public";
|
||||
import { Position, Whirlpool } from "../../whirlpool-client";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
export * from "./pda-utils";
|
||||
export * from "./price-math";
|
||||
export * from "./tick-utils";
|
||||
export * from "./pool-utils";
|
||||
export * from "./ix-utils";
|
||||
export * from "./types";
|
||||
export * from "./pda-utils";
|
||||
export * from "./pool-utils";
|
||||
export * from "./price-math";
|
||||
export * from "./swap-utils";
|
||||
export * from "./tick-utils";
|
||||
export * from "./types";
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { ASSOCIATED_TOKEN_PROGRAM_ID, NATIVE_MINT, TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
|
||||
export function isNativeMint(mint: PublicKey) {
|
||||
return mint.equals(NATIVE_MINT);
|
||||
}
|
||||
|
||||
// TODO: Update spl-token so we get this method
|
||||
export function getAssociatedTokenAddressSync(
|
||||
mint: string,
|
||||
owner: string,
|
||||
programId = TOKEN_PROGRAM_ID,
|
||||
associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID
|
||||
): PublicKey {
|
||||
const [address] = PublicKey.findProgramAddressSync(
|
||||
[new PublicKey(owner).toBuffer(), programId.toBuffer(), new PublicKey(mint).toBuffer()],
|
||||
associatedTokenProgramId
|
||||
);
|
||||
|
||||
return address;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
export function convertListToMap<T>(fetchedData: T[], addresses: string[]) {
|
||||
const result: Record<string, T> = {};
|
||||
fetchedData.forEach((data, index) => {
|
||||
if (data) {
|
||||
const addr = addresses[index];
|
||||
result[addr] = data;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import { Percentage, TransactionBuilder } from "@orca-so/common-sdk";
|
||||
import { Address } from "@project-serum/anchor";
|
||||
import { u64 } from "@solana/spl-token";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import { WhirlpoolContext } from "./context";
|
||||
import { WhirlpoolClientImpl } from "./impl/whirlpool-client-impl";
|
||||
|
@ -10,7 +9,7 @@ import {
|
|||
DecreaseLiquidityInput,
|
||||
IncreaseLiquidityInput,
|
||||
PositionData,
|
||||
WhirlpoolData,
|
||||
WhirlpoolData
|
||||
} from "./types/public";
|
||||
import { TokenAccountInfo, TokenInfo, WhirlpoolRewardInfo } from "./types/public/client-types";
|
||||
|
||||
|
@ -68,6 +67,19 @@ export interface WhirlpoolClient {
|
|||
refresh?: boolean
|
||||
) => Promise<Record<string, Position | null>>;
|
||||
|
||||
/**
|
||||
* Collect all fees and rewards from a list of positions.
|
||||
* @experimental
|
||||
* @param positionAddress the addresses of the Position accounts to collect fee & rewards from.
|
||||
* @param refresh true to always request newest data from chain with this request
|
||||
* @returns A set of transaction-builders to resolve ATA for affliated tokens, collect fee & rewards for all positions.
|
||||
* The first transaction should always be processed as it contains all the resolve ATA instructions to receive tokens.
|
||||
*/
|
||||
collectFeesAndRewardsForPositions: (
|
||||
positionAddresses: Address[],
|
||||
refresh?: boolean
|
||||
) => Promise<TransactionBuilder[]>;
|
||||
|
||||
/**
|
||||
* Create a Whirlpool account for a group of token A, token B and tick spacing
|
||||
* @param whirlpoolConfig the address of the whirlpool config
|
||||
|
|
Loading…
Reference in New Issue