Add collectFeesAndRewardsForPositions to allow easy fee/reward collection (#57)

- Add collectFeesAndRewardsForPositions to WhirlpoolClient
- Add collectAllForPositionAddressesTxns to WhirlpoolIx
This commit is contained in:
meep 2022-11-24 15:37:41 +07:00 committed by GitHub
parent 8a7ed57bc5
commit 3c7b90c147
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1012 additions and 705 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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;
}

View File

@ -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
);
}
}

View File

@ -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
);

View File

@ -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

View File

@ -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;
};

View File

@ -0,0 +1,2 @@
export * from "./collect-all-txn";
export * from "./resolve-atas-ix";

View File

@ -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)
),
};
}

View File

@ -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";

View File

@ -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);
}
}

View File

@ -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";

View File

@ -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";

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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

1097
yarn.lock

File diff suppressed because it is too large Load Diff