@renec-foundation/redex-sdk
Version:
Typescript SDK to interact with Orca's Whirlpool program.
191 lines (190 loc) • 10.6 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.collectAllForPositionsTxns = exports.collectAllForPositionAddressesTxns = void 0;
const common_sdk_1 = require("@orca-so/common-sdk");
const spl_token_1 = require("@solana/spl-token");
const web3_js_1 = require("@solana/web3.js");
const ix_1 = require("../../ix");
const public_1 = require("../../utils/public");
const spl_token_utils_1 = require("../../utils/spl-token-utils");
const txn_utils_1 = require("../../utils/txn-utils");
const whirlpool_ata_utils_1 = require("../../utils/whirlpool-ata-utils");
const update_fees_and_rewards_ix_1 = require("../update-fees-and-rewards-ix");
/**
* 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.
*/
function collectAllForPositionAddressesTxns(ctx, params, refresh = false) {
return __awaiter(this, void 0, void 0, function* () {
const { positions } = params, rest = __rest(params, ["positions"]);
const posData = (0, txn_utils_1.convertListToMap)(yield ctx.fetcher.listPositions(positions, refresh), positions.map((pos) => pos.toString()));
const positionMap = {};
Object.entries(posData).forEach(([addr, pos]) => {
if (pos) {
positionMap[addr] = pos;
}
});
return collectAllForPositionsTxns(ctx, Object.assign({ positions: positionMap }, rest));
});
}
exports.collectAllForPositionAddressesTxns = collectAllForPositionAddressesTxns;
/**
* 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.
*/
function collectAllForPositionsTxns(ctx, params) {
return __awaiter(this, void 0, void 0, function* () {
const { positions, receiver, positionAuthority, positionOwner, payer } = params;
const receiverKey = receiver !== null && receiver !== void 0 ? receiver : ctx.wallet.publicKey;
const positionAuthorityKey = positionAuthority !== null && positionAuthority !== void 0 ? positionAuthority : ctx.wallet.publicKey;
const positionOwnerKey = positionOwner !== null && positionOwner !== void 0 ? positionOwner : ctx.wallet.publicKey;
const payerKey = payer !== null && payer !== void 0 ? payer : ctx.wallet.publicKey;
const positionList = Object.entries(positions);
if (positionList.length === 0) {
return [];
}
const whirlpoolAddrs = positionList.map(([, pos]) => pos.whirlpool.toBase58());
const whirlpoolDatas = yield ctx.fetcher.listPools(whirlpoolAddrs, false);
const whirlpools = (0, txn_utils_1.convertListToMap)(whirlpoolDatas, whirlpoolAddrs);
const allMints = (0, whirlpool_ata_utils_1.getTokenMintsFromWhirlpools)(whirlpoolDatas);
const accountExemption = yield ctx.fetcher.getAccountRentExempt();
// resolvedAtas[mint] => Instruction & { address }
// if already ATA exists, Instruction will be EMPTY_INSTRUCTION
const resolvedAtas = (0, txn_utils_1.convertListToMap)(yield (0, common_sdk_1.resolveOrCreateATAs)(ctx.connection, receiverKey, allMints.mintMap.map((tokenMint) => ({ tokenMint })), () => __awaiter(this, void 0, void 0, function* () { return accountExemption; }), payerKey, false // CreateIdempotent
), allMints.mintMap.map((mint) => mint.toBase58()));
const latestBlockhash = yield ctx.connection.getLatestBlockhash("singleGossip");
const txBuilders = [];
let posIndex = 0;
let pendingTxBuilder = null;
let touchedMints = null;
let reattempt = false;
while (posIndex < positionList.length) {
if (!pendingTxBuilder || !touchedMints) {
pendingTxBuilder = new common_sdk_1.TransactionBuilder(ctx.connection, ctx.wallet);
touchedMints = new Set();
resolvedAtas[spl_token_1.NATIVE_MINT.toBase58()] = (0, spl_token_utils_1.createWSOLAccountInstructions)(receiverKey, common_sdk_1.ZERO, accountExemption);
}
// Build collect instructions
const [positionAddr, position] = positionList[posIndex];
const collectIxForPosition = constructCollectIxForPosition(ctx, new web3_js_1.PublicKey(positionAddr), position, whirlpools, positionOwnerKey, positionAuthorityKey, resolvedAtas, touchedMints);
const positionTxBuilder = new common_sdk_1.TransactionBuilder(ctx.connection, ctx.wallet);
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 mergeable = yield (0, txn_utils_1.checkMergedTransactionSizeIsValid)(ctx, [pendingTxBuilder, positionTxBuilder], latestBlockhash);
if (mergeable) {
pendingTxBuilder.addInstruction(positionTxBuilder.compressIx(false));
posIndex += 1;
reattempt = false;
}
else {
if (reattempt) {
throw new Error(`Unable to fit collection ix for ${position.positionMint.toBase58()} in a Transaction.`);
}
txBuilders.push(pendingTxBuilder);
pendingTxBuilder = null;
touchedMints = null;
reattempt = true;
}
}
if (pendingTxBuilder) {
txBuilders.push(pendingTxBuilder);
}
return txBuilders;
});
}
exports.collectAllForPositionsTxns = collectAllForPositionsTxns;
// TODO: Once individual collect ix for positions is implemented, maybe migrate over if it can take custom ATA?
const constructCollectIxForPosition = (ctx, positionKey, position, whirlpools, positionOwner, positionAuthority, resolvedAtas, touchedMints) => {
const ixForPosition = [];
const { whirlpool: whirlpoolKey, liquidity, tickLowerIndex, tickUpperIndex, positionMint, rewardInfos: positionRewardInfos, } = position;
const whirlpool = whirlpools[whirlpoolKey.toBase58()];
if (!whirlpool) {
throw new Error(`Unable to process positionMint ${positionMint} - unable to derive whirlpool ${whirlpoolKey.toBase58()}`);
}
const { tickSpacing } = whirlpool;
const mintA = whirlpool.tokenMintA.toBase58();
const mintB = whirlpool.tokenMintB.toBase58();
const positionTokenAccount = (0, spl_token_utils_1.getAssociatedTokenAddressSync)(positionMint.toBase58(), positionOwner.toBase58());
// Update fee and reward values if necessary
if (!liquidity.eq(common_sdk_1.ZERO)) {
ixForPosition.push((0, update_fees_and_rewards_ix_1.updateFeesAndRewardsIx)(ctx.program, {
position: positionKey,
whirlpool: whirlpoolKey,
tickArrayLower: public_1.PDAUtil.getTickArray(ctx.program.programId, whirlpoolKey, public_1.TickUtil.getStartTickIndex(tickLowerIndex, tickSpacing)).publicKey,
tickArrayUpper: public_1.PDAUtil.getTickArray(ctx.program.programId, whirlpoolKey, public_1.TickUtil.getStartTickIndex(tickUpperIndex, tickSpacing)).publicKey,
}));
}
// Collect Fee
if (!touchedMints.has(mintA)) {
ixForPosition.push(resolvedAtas[mintA]);
touchedMints.add(mintA);
}
if (!touchedMints.has(mintB)) {
ixForPosition.push(resolvedAtas[mintB]);
touchedMints.add(mintB);
}
ixForPosition.push(ix_1.WhirlpoolIx.collectFeesIx(ctx.program, {
whirlpool: whirlpoolKey,
position: positionKey,
positionAuthority,
positionTokenAccount,
tokenOwnerAccountA: resolvedAtas[mintA].address,
tokenOwnerAccountB: resolvedAtas[mintB].address,
tokenVaultA: whirlpool.tokenVaultA,
tokenVaultB: whirlpool.tokenVaultB,
}));
// Collect Rewards
// TODO: handle empty vault values?
positionRewardInfos.forEach((_, index) => {
const rewardInfo = whirlpool.rewardInfos[index];
if (public_1.PoolUtil.isRewardInitialized(rewardInfo)) {
const mintReward = rewardInfo.mint.toBase58();
if (!touchedMints.has(mintReward)) {
ixForPosition.push(resolvedAtas[mintReward]);
touchedMints.add(mintReward);
}
ixForPosition.push(ix_1.WhirlpoolIx.collectRewardIx(ctx.program, {
whirlpool: whirlpoolKey,
position: positionKey,
positionAuthority,
positionTokenAccount,
rewardIndex: index,
rewardOwnerAccount: resolvedAtas[mintReward].address,
rewardVault: rewardInfo.vault,
}));
}
});
return ixForPosition;
};