UNPKG

@renec-foundation/redex-sdk

Version:

Typescript SDK to interact with Orca's Whirlpool program.

191 lines (190 loc) 10.6 kB
"use strict"; 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; };