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

Add whirlpools cli package #492

Merged
merged 1 commit into from
Nov 12, 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
73 changes: 73 additions & 0 deletions legacy-sdk/cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Setting up your script environment
```bash
yarn
```

# Set your RPC and wallet
```bash
export ANCHOR_PROVIDER_URL=<RPC URL>
export ANCHOR_WALLET=<WALLET JSON PATH>
```

Example:
```bash
export ANCHOR_PROVIDER_URL=http://localhost:8899
export ANCHOR_WALLET=~/.config/solana/id.json
```

# Supported commands
Token-2022 tokens are acceptable 👍

## Config & FeeTier
### initialize
- `yarn start initializeConfig`: initialize new WhirlpoolsConfig account
- `yarn start initializeConfigExtension`: initialize new WhirlpoolsConfigExtension account
- `yarn start initializeFeeTier`: initialize new FeeTier account

### update
- `yarn start setTokenBadgeAuthority`: set new TokenBadge authority on WhirlpoolsConfigExtension
- `yarn start setDefaultProtocolFeeRate`: set new default protocol fee rate on WhirlpoolsConfig
- `yarn start setFeeAuthority`: set new fee authority on WhirlpoolsConfig
- `yarn start setCollectProtocolFeesAuthority`: set new collect protocol fees authority on WhirlpoolsConfig
- TODO: set reward emissions super authority
- TODO: set config extension authority

## Whirlpool & TickArray
- `yarn start initializeWhirlpool`: initialize new Whirlpool account
- `yarn start initializeTickArray`: initialize new TickArray account

## TokenBadge
- `yarn start initializeTokenBadge`: initialize new TokenBadge account
- `yarn start deleteTokenBadge`: delete TokenBadge account

## Reward
- `yarn start initializeReward`: initialize new reward for a whirlpool
- TODO: set reward emission

## Position
- `yarn start openPosition`: open a new position
- `yarn start increaseLiquidity`: deposit to a position
- `yarn start decreaseLiquidity`: withdraw from a position
- `yarn start collectFees`: collect fees from a position
- `yarn start collectRewards`: collect rewards from a position
- `yarn start closePosition`: close an empty position

## Swap
- `yarn start pushPrice`: adjust pool price (possible if pool liquidity is zero or very small)

## WSOL and ATA creation
TODO: WSOL handling & create ATA if needed (workaround exists, please see the following)

### workaround for WSOL
`whirlpool-mgmt-tools` works well with ATA, so using WSOL on ATA is workaround.

- wrap 1 SOL: `spl-token wrap 1` (ATA for WSOL will be initialized with 1 SOL)
- unwrap: `spl-token unwrap` (ATA for WSOL will be closed)
- add 1 WSOL: `solana transfer <WSOL ATA address> 1` then `spl-token sync-native` (transfer & sync are needed)

### workaround for ATA
We can easily initialize ATA with spl-token CLI.

```
spl-token create-account <mint address>
```
28 changes: 28 additions & 0 deletions legacy-sdk/cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@orca-so/whirlpools-sdk-cli",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"build": "tsc --noEmit",
"start": "tsx src/index.ts"
},
"dependencies": {
"@coral-xyz/anchor": "0.29.0",
"@orca-so/common-sdk": "0.6.3",
"@orca-so/orca-sdk": "0.1.1",
"@orca-so/whirlpools-sdk": "*",
"@solana/spl-token": "0.4.1",
"@solana/web3.js": "^1.90.0",
"@types/bn.js": "^5.1.0",
"bs58": "^5.0.0",
"decimal.js": "^10.4.3",
"js-convert-case": "^4.2.0",
"prompts": "^2.4.2"
},
"devDependencies": {
"@types/prompts": "^2.4.9",
"tsx": "^4.19.0",
"typescript": "^5.4.5"
}
}
92 changes: 92 additions & 0 deletions legacy-sdk/cli/src/commands/close_position.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { PublicKey } from "@solana/web3.js";
import { WhirlpoolIx } from "@orca-so/whirlpools-sdk";
import { TransactionBuilder } from "@orca-so/common-sdk";
import {
getAssociatedTokenAddressSync,
TOKEN_2022_PROGRAM_ID,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { sendTransaction } from "../utils/transaction_sender";
import { ctx } from "../utils/provider";
import { promptText } from "../utils/prompt";

console.info("close Position...");

// prompt
const positionPubkeyStr = await promptText("positionPubkey");

const positionPubkey = new PublicKey(positionPubkeyStr);
const position = await ctx.fetcher.getPosition(positionPubkey);
if (!position) {
throw new Error("position not found");
}
const positionMint = await ctx.fetcher.getMintInfo(position.positionMint);
if (!positionMint) {
throw new Error("positionMint not found");
}

if (!position.liquidity.isZero()) {
throw new Error("position is not empty (liquidity is not zero)");
}

if (!position.feeOwedA.isZero() || !position.feeOwedB.isZero()) {
throw new Error("position has collectable fees");
}

if (!position.rewardInfos.every((r) => r.amountOwed.isZero())) {
throw new Error("position has collectable rewards");
}

const builder = new TransactionBuilder(ctx.connection, ctx.wallet);

if (positionMint.tokenProgram.equals(TOKEN_PROGRAM_ID)) {
builder.addInstruction(
WhirlpoolIx.closePositionIx(ctx.program, {
position: positionPubkey,
positionAuthority: ctx.wallet.publicKey,
positionTokenAccount: getAssociatedTokenAddressSync(
position.positionMint,
ctx.wallet.publicKey,
),
positionMint: position.positionMint,
receiver: ctx.wallet.publicKey,
}),
);
} else {
builder.addInstruction(
WhirlpoolIx.closePositionWithTokenExtensionsIx(ctx.program, {
position: positionPubkey,
positionAuthority: ctx.wallet.publicKey,
positionTokenAccount: getAssociatedTokenAddressSync(
position.positionMint,
ctx.wallet.publicKey,
undefined,
TOKEN_2022_PROGRAM_ID,
),
positionMint: position.positionMint,
receiver: ctx.wallet.publicKey,
}),
);
}

await sendTransaction(builder);

/*

SAMPLE EXECUTION LOG

connection endpoint http://localhost:8899
wallet r21Gamwd9DtyjHeGywsneoQYR39C1VDwrw7tWxHAwh6
close Position...
prompt: positionPubkey: H4WEb57EYh5AhorHArjgRXVgSBJRMZi3DvsLb3J1XNj6
estimatedComputeUnits: 120649
prompt: priorityFeeInSOL: 0
Priority fee: 0 SOL
process transaction...
transaction is still valid, 150 blocks left (at most)
sending...
confirming...
✅successfully landed
signature dQwedycTbM9UTYwQiiUE5Q7ydZRzL3zywaQ3xEo3RhHxDvfsY8wkAakSXQRdXswxdQCLLMwwDJVSNHYcTCDDcf3

*/
182 changes: 182 additions & 0 deletions legacy-sdk/cli/src/commands/collect_fees.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { PublicKey } from "@solana/web3.js";
import {
ORCA_WHIRLPOOL_PROGRAM_ID,
PDAUtil,
WhirlpoolIx,
collectFeesQuote,
TickArrayUtil,
} from "@orca-so/whirlpools-sdk";
import { DecimalUtil, TransactionBuilder } from "@orca-so/common-sdk";
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
import { sendTransaction } from "../utils/transaction_sender";
import { TokenExtensionUtil } from "@orca-so/whirlpools-sdk/dist/utils/public/token-extension-util";
import { ctx } from "../utils/provider";
import { promptText } from "../utils/prompt";

console.info("collect Fees...");

// prompt
const positionPubkeyStr = await promptText("positionPubkey");

const positionPubkey = new PublicKey(positionPubkeyStr);
const position = await ctx.fetcher.getPosition(positionPubkey);
if (!position) {
throw new Error("position not found");
}
const positionMint = await ctx.fetcher.getMintInfo(position.positionMint);
if (!positionMint) {
throw new Error("positionMint not found");
}

const whirlpoolPubkey = position.whirlpool;
const whirlpool = await ctx.fetcher.getPool(whirlpoolPubkey);
if (!whirlpool) {
throw new Error("whirlpool not found");
}
const tickSpacing = whirlpool.tickSpacing;

const tokenMintAPubkey = whirlpool.tokenMintA;
const tokenMintBPubkey = whirlpool.tokenMintB;
const mintA = await ctx.fetcher.getMintInfo(tokenMintAPubkey);
const mintB = await ctx.fetcher.getMintInfo(tokenMintBPubkey);
if (!mintA || !mintB) {
// extremely rare case (CloseMint extension on Token-2022 is used)
throw new Error("token mint not found");
}
const decimalsA = mintA.decimals;
const decimalsB = mintB.decimals;

const lowerTickArrayPubkey = PDAUtil.getTickArrayFromTickIndex(
position.tickLowerIndex,
tickSpacing,
whirlpoolPubkey,
ORCA_WHIRLPOOL_PROGRAM_ID,
).publicKey;
const upperTickArrayPubkey = PDAUtil.getTickArrayFromTickIndex(
position.tickUpperIndex,
tickSpacing,
whirlpoolPubkey,
ORCA_WHIRLPOOL_PROGRAM_ID,
).publicKey;

const lowerTickArray = await ctx.fetcher.getTickArray(lowerTickArrayPubkey);
const upperTickArray = await ctx.fetcher.getTickArray(upperTickArrayPubkey);
if (!lowerTickArray || !upperTickArray) {
throw new Error("tick array not found");
}

const quote = collectFeesQuote({
position,
tickLower: TickArrayUtil.getTickFromArray(
lowerTickArray,
position.tickLowerIndex,
tickSpacing,
),
tickUpper: TickArrayUtil.getTickFromArray(
upperTickArray,
position.tickUpperIndex,
tickSpacing,
),
whirlpool,
tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(
ctx.fetcher,
whirlpool,
),
});

console.info(
"collectable feeA: ",
DecimalUtil.fromBN(quote.feeOwedA, decimalsA),
);
console.info(
"collectable feeB: ",
DecimalUtil.fromBN(quote.feeOwedB, decimalsB),
);

const builder = new TransactionBuilder(ctx.connection, ctx.wallet);

const tokenOwnerAccountA = getAssociatedTokenAddressSync(
tokenMintAPubkey,
ctx.wallet.publicKey,
undefined,
mintA.tokenProgram,
);
const tokenOwnerAccountB = getAssociatedTokenAddressSync(
tokenMintBPubkey,
ctx.wallet.publicKey,
undefined,
mintB.tokenProgram,
);

if (position.liquidity.gtn(0)) {
builder.addInstruction(
WhirlpoolIx.updateFeesAndRewardsIx(ctx.program, {
position: positionPubkey,
tickArrayLower: lowerTickArrayPubkey,
tickArrayUpper: upperTickArrayPubkey,
whirlpool: whirlpoolPubkey,
}),
);
}

builder.addInstruction(
WhirlpoolIx.collectFeesV2Ix(ctx.program, {
position: positionPubkey,
positionAuthority: ctx.wallet.publicKey,
tokenMintA: tokenMintAPubkey,
tokenMintB: tokenMintBPubkey,
positionTokenAccount: getAssociatedTokenAddressSync(
position.positionMint,
ctx.wallet.publicKey,
undefined,
positionMint.tokenProgram,
),
tokenOwnerAccountA,
tokenOwnerAccountB,
tokenProgramA: mintA.tokenProgram,
tokenProgramB: mintB.tokenProgram,
tokenVaultA: whirlpool.tokenVaultA,
tokenVaultB: whirlpool.tokenVaultB,
whirlpool: whirlpoolPubkey,
tokenTransferHookAccountsA:
await TokenExtensionUtil.getExtraAccountMetasForTransferHook(
ctx.provider.connection,
mintA,
tokenOwnerAccountA,
whirlpool.tokenVaultA,
ctx.wallet.publicKey,
),
tokenTransferHookAccountsB:
await TokenExtensionUtil.getExtraAccountMetasForTransferHook(
ctx.provider.connection,
mintB,
tokenOwnerAccountB,
whirlpool.tokenVaultB,
ctx.wallet.publicKey,
),
}),
);

await sendTransaction(builder);

/*

SAMPLE EXECUTION LOG

connection endpoint http://localhost:8899
wallet r21Gamwd9DtyjHeGywsneoQYR39C1VDwrw7tWxHAwh6
collect Fees...
prompt: positionPubkey: H4WEb57EYh5AhorHArjgRXVgSBJRMZi3DvsLb3J1XNj6
collectable feeA: 0
collectable feeB: 0
estimatedComputeUnits: 149469
prompt: priorityFeeInSOL: 0
Priority fee: 0 SOL
process transaction...
transaction is still valid, 150 blocks left (at most)
sending...
confirming...
✅successfully landed
signature 3VfNAQJ8nxTStU9fjkhg5sNRPpBrAMYx5Vyp92aDS5FsWpEqgLw5Ckzzw5hJ1rsNEh6VGLaf9TZWWcLCRWzvhNjX

*/
Loading