Skip to content

Commit

Permalink
TokenExtensions based position NFT (#341)
Browse files Browse the repository at this point in the history
* initial impl position 2022

* allow TE mint at position ops

* rename verify_position_authority fn

* fix warnings, initialize MetadataPointer if metadata required

* instruction name change

issue on conversion between camel and snake (pos_2022 >> pos2022 >> pos2022) and generate wrong instruction disc code (instruction not found)

* add test cases for open_position_with_token_extensions

* add test cases for close_position_with_token_extensions

* fix build error

* add test cases (life cycle test)

* refactor

* cargo fmt

* update rich client functions

* add test cases for rich client

* add test cases for rich client

* fix lint error

* add getAllPositionAccountsByOwner

* add test cases for getAllPositionAccountsByOwner

* update position NFT metadata URI

* fix lint error

* address review comments

* fix getAllPositionAccountsByOwner test case (isolate test env)

* increase accrue reward time to avoid accidental failure

* eliminate fixed length account init by removing WP_2022_METADATA_MAX_LEN

* fix lint error

* reserve FreezeAuthority

* switch withTokenExtensions to tokenProgramId on client

* fix: not skip simulation on test

* bump versions (contract & sdk)

* bump sdk version 0.13.8
  • Loading branch information
yugure-orca authored Oct 8, 2024
1 parent 69eb2f6 commit 5981063
Show file tree
Hide file tree
Showing 50 changed files with 4,320 additions and 86 deletions.
7 changes: 4 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion legacy-sdk/whirlpool/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@orca-so/whirlpools-sdk",
"version": "0.13.7",
"version": "0.13.8",
"description": "Typescript SDK to interact with Orca's Whirlpool program.",
"license": "Apache-2.0",
"main": "dist/index.js",
Expand Down
9 changes: 9 additions & 0 deletions legacy-sdk/whirlpool/src/impl/position-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export class PositionImpl implements Position {
whirlpoolData: WhirlpoolData,
lowerTickArrayData: TickArrayData,
upperTickArrayData: TickArrayData,
readonly positionMintTokenProgramId: PublicKey,
) {
this.data = data;
this.whirlpoolData = whirlpoolData;
Expand All @@ -74,6 +75,10 @@ export class PositionImpl implements Position {
return this.address;
}

getPositionMintTokenProgramId(): PublicKey {
return this.positionMintTokenProgramId;
}

getData(): PositionData {
return this.data;
}
Expand Down Expand Up @@ -188,6 +193,7 @@ export class PositionImpl implements Position {
this.data.positionMint,
positionWalletKey,
this.ctx.accountResolverOpts.allowPDAOwnerAddress,
this.positionMintTokenProgramId,
);

const baseParams = {
Expand Down Expand Up @@ -324,6 +330,7 @@ export class PositionImpl implements Position {
this.data.positionMint,
positionWalletKey,
this.ctx.accountResolverOpts.allowPDAOwnerAddress,
this.positionMintTokenProgramId,
),
tokenOwnerAccountA,
tokenOwnerAccountB,
Expand Down Expand Up @@ -457,6 +464,7 @@ export class PositionImpl implements Position {
this.data.positionMint,
positionWalletKey,
this.ctx.accountResolverOpts.allowPDAOwnerAddress,
this.positionMintTokenProgramId,
);

if (updateFeesAndRewards && !this.data.liquidity.isZero()) {
Expand Down Expand Up @@ -578,6 +586,7 @@ export class PositionImpl implements Position {
this.data.positionMint,
positionWalletKey,
this.ctx.accountResolverOpts.allowPDAOwnerAddress,
this.positionMintTokenProgramId,
);

if (updateFeesAndRewards && !this.data.liquidity.isZero()) {
Expand Down
12 changes: 12 additions & 0 deletions legacy-sdk/whirlpool/src/impl/whirlpool-client-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ export class WhirlpoolClientImpl implements WhirlpoolClient {
);
}

const positionMint = await this.ctx.fetcher.getMintInfo(account.positionMint, opts);
if (!positionMint) {
throw new Error(
`Unable to fetch Mint for Position at address at ${positionAddress}`,
);
}

const [lowerTickArray, upperTickArray] = await getTickArrayDataForPosition(
this.ctx,
account,
Expand All @@ -178,6 +185,7 @@ export class WhirlpoolClientImpl implements WhirlpoolClient {
whirlAccount,
lowerTickArray,
upperTickArray,
positionMint.tokenProgram,
);
}

Expand All @@ -193,6 +201,10 @@ export class WhirlpoolClientImpl implements WhirlpoolClient {
.map((position) => position?.whirlpool.toBase58())
.flatMap((x) => (!!x ? x : []));
await this.ctx.fetcher.getPools(whirlpoolAddrs, opts);
const positionMintAddrs = positions
.map((position) => position?.positionMint.toBase58())
.flatMap((x) => (!!x ? x : []));
await this.ctx.fetcher.getMintInfos(positionMintAddrs, opts);
const tickArrayAddresses: Set<string> = new Set();
await Promise.all(
positions.map(async (pos) => {
Expand Down
57 changes: 47 additions & 10 deletions legacy-sdk/whirlpool/src/impl/whirlpool-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
ZERO,
resolveOrCreateATAs,
} from "@orca-so/common-sdk";
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
import { getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token";
import type { PublicKey } from "@solana/web3.js";
import { Keypair } from "@solana/web3.js";
import invariant from "tiny-invariant";
Expand All @@ -20,11 +20,13 @@ import type {
} from "../instructions";
import {
closePositionIx,
closePositionWithTokenExtensionsIx,
increaseLiquidityIx,
increaseLiquidityV2Ix,
initTickArrayIx,
openPositionIx,
openPositionWithMetadataIx,
openPositionWithTokenExtensionsIx,
swapAsync,
} from "../instructions";
import { WhirlpoolIx } from "../ix";
Expand Down Expand Up @@ -111,6 +113,7 @@ export class WhirlpoolImpl implements Whirlpool {
wallet?: Address,
funder?: Address,
positionMint?: PublicKey,
tokenProgramId?: PublicKey,
) {
await this.refresh();
return this.getOpenPositionWithOptMetadataTx(
Expand All @@ -119,6 +122,8 @@ export class WhirlpoolImpl implements Whirlpool {
liquidityInput,
!!wallet ? AddressUtil.toPubKey(wallet) : this.ctx.wallet.publicKey,
!!funder ? AddressUtil.toPubKey(funder) : this.ctx.wallet.publicKey,
// TOKEN_PROGRAM_ID for v0.13.x, TOKEN_2022_PROGRAM_ID for future releases
tokenProgramId ?? TOKEN_PROGRAM_ID,
false,
positionMint,
);
Expand All @@ -131,6 +136,7 @@ export class WhirlpoolImpl implements Whirlpool {
sourceWallet?: Address,
funder?: Address,
positionMint?: PublicKey,
tokenProgramId?: PublicKey,
) {
await this.refresh();
return this.getOpenPositionWithOptMetadataTx(
Expand All @@ -141,6 +147,8 @@ export class WhirlpoolImpl implements Whirlpool {
? AddressUtil.toPubKey(sourceWallet)
: this.ctx.wallet.publicKey,
!!funder ? AddressUtil.toPubKey(funder) : this.ctx.wallet.publicKey,
// TOKEN_PROGRAM_ID for v0.13.x, TOKEN_2022_PROGRAM_ID for future releases
tokenProgramId ?? TOKEN_PROGRAM_ID,
true,
positionMint,
);
Expand Down Expand Up @@ -294,6 +302,7 @@ export class WhirlpoolImpl implements Whirlpool {
liquidityInput: IncreaseLiquidityInput,
wallet: PublicKey,
funder: PublicKey,
tokenProgramId: PublicKey,
withMetadata: boolean = false,
positionMint?: PublicKey,
): Promise<{ positionMint: PublicKey; tx: TransactionBuilder }> {
Expand All @@ -305,6 +314,10 @@ export class WhirlpoolImpl implements Whirlpool {
TickUtil.checkTickInBounds(tickUpper),
"tickUpper is out of bounds.",
);
invariant(
tokenProgramId.equals(TOKEN_PROGRAM_ID) || tokenProgramId.equals(TOKEN_2022_PROGRAM_ID),
"tokenProgramId must be either TOKEN_PROGRAM_ID or TOKEN_2022_PROGRAM_ID",
);

const { liquidityAmount: liquidity, tokenMaxA, tokenMaxB } = liquidityInput;

Expand Down Expand Up @@ -347,6 +360,7 @@ export class WhirlpoolImpl implements Whirlpool {
positionMintPubkey,
wallet,
this.ctx.accountResolverOpts.allowPDAOwnerAddress,
tokenProgramId,
);

const txBuilder = new TransactionBuilder(
Expand All @@ -355,20 +369,28 @@ export class WhirlpoolImpl implements Whirlpool {
this.ctx.txBuilderOpts,
);

const positionIx = (
withMetadata ? openPositionWithMetadataIx : openPositionIx
)(this.ctx.program, {
const params = {
funder,
owner: wallet,
positionPda,
metadataPda,
positionMintAddress: positionMintPubkey,
positionTokenAccount: positionTokenAccountAddress,
whirlpool: this.address,
tickLowerIndex: tickLower,
tickUpperIndex: tickUpper,
});
};
const positionIx = tokenProgramId.equals(TOKEN_2022_PROGRAM_ID)
? openPositionWithTokenExtensionsIx(this.ctx.program, {
...params,
positionMint: positionMintPubkey,
withTokenMetadataExtension: withMetadata,
})
: (withMetadata ? openPositionWithMetadataIx : openPositionIx)(this.ctx.program, {
...params,
positionMintAddress: positionMintPubkey,
metadataPda,
});
txBuilder.addInstruction(positionIx);

if (positionMint === undefined) {
txBuilder.addSigner(positionMintKeypair);
}
Expand Down Expand Up @@ -466,6 +488,15 @@ export class WhirlpoolImpl implements Whirlpool {
throw new Error(`Position not found: ${positionAddress.toBase58()}`);
}

const positionMint = await this.ctx.fetcher.getMintInfo(
positionData.positionMint,
);
if (!positionMint) {
throw new Error(
`Position mint not found: ${positionData.positionMint.toBase58()}`,
);
}

const whirlpool = this.data;

invariant(
Expand All @@ -477,6 +508,7 @@ export class WhirlpoolImpl implements Whirlpool {
positionData.positionMint,
positionWallet,
this.ctx.accountResolverOpts.allowPDAOwnerAddress,
positionMint.tokenProgram,
);

const accountExemption = await this.ctx.fetcher.getAccountRentExempt();
Expand Down Expand Up @@ -527,6 +559,7 @@ export class WhirlpoolImpl implements Whirlpool {
whirlpool,
tickArrayLowerData,
tickArrayUpperData,
positionMint.tokenProgram,
);

const tickLower = position.getLowerTickData();
Expand Down Expand Up @@ -769,15 +802,19 @@ export class WhirlpoolImpl implements Whirlpool {

/* Close position */
await builder.addInstructions(async () => {
const ix = closePositionIx(this.ctx.program, {
const closePositionParams = {
positionAuthority: positionWallet,
receiver: destinationWallet,
positionTokenAccount,
position: positionAddress,
positionMint: positionData.positionMint,
});
};

return [ix];
if (positionMint.tokenProgram.equals(TOKEN_2022_PROGRAM_ID)) {
return [closePositionWithTokenExtensionsIx(this.ctx.program, closePositionParams)]
} else {
return [closePositionIx(this.ctx.program, closePositionParams)];
}
});

return builder.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { Program } from "@coral-xyz/anchor";
import type { Instruction } from "@orca-so/common-sdk";
import { TOKEN_2022_PROGRAM_ID } from "@solana/spl-token";
import type { PublicKey } from "@solana/web3.js";
import type { Whirlpool } from "../artifacts/whirlpool";

/**
* Parameters to close a position (based on Token-2022) in a Whirlpool.
*
* @category Instruction Types
* @param receiver - PublicKey for the wallet that will receive the rented lamports.
* @param position - PublicKey for the position.
* @param positionMint - PublicKey for the mint token for the Position token.
* @param positionTokenAccount - The associated token address for the position token in the owners wallet.
* @param positionAuthority - Authority that owns the position token.
*/
export type ClosePositionWithTokenExtensionsParams = {
receiver: PublicKey;
position: PublicKey;
positionMint: PublicKey;
positionTokenAccount: PublicKey;
positionAuthority: PublicKey;
};

/**
* Close a position in a Whirlpool. Burns the position token in the owner's wallet.
* Mint and TokenAccount are based on Token-2022. And Mint accout will be also closed.
*
* @category Instructions
* @param context - Context object containing services required to generate the instruction
* @param params - ClosePositionWithTokenExtensionsParams object
* @returns - Instruction to perform the action.
*/
export function closePositionWithTokenExtensionsIx(
program: Program<Whirlpool>,
params: ClosePositionWithTokenExtensionsParams,
): Instruction {
const ix = program.instruction.closePositionWithTokenExtensions({
accounts: {
...params,
token2022Program: TOKEN_2022_PROGRAM_ID,
},
});

return {
instructions: [ix],
cleanupInstructions: [],
signers: [],
};
}
Loading

0 comments on commit 5981063

Please sign in to comment.