Skip to content

Commit 7af6810

Browse files
committed
Add unit test for transfer_locked_position instruction in ts-sdk
1 parent 4a35de0 commit 7af6810

File tree

4 files changed

+174
-2
lines changed

4 files changed

+174
-2
lines changed

.changeset/neat-groups-slide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@orca-so/whirlpools": patch
3+
---
4+
5+
Implements transfer_locked_position_instruction in ts-sdk

.changeset/slimy-ears-type.md

Lines changed: 0 additions & 2 deletions
This file was deleted.

ts-sdk/whirlpool/src/transferLockedPosition.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
TransactionSigner,
1212
} from "@solana/kit";
1313
import { FUNDER } from "./config";
14+
import { wrapFunctionWithExecution } from "./actionHelpers";
1415

1516
/**
1617
* Parameters for transferring a locked position.
@@ -94,3 +95,9 @@ export async function transferLockedPositionInstructions(
9495
instructions,
9596
};
9697
}
98+
99+
// -------- ACTIONS --------
100+
101+
export const transferLockedPosition = wrapFunctionWithExecution(
102+
transferLockedPositionInstructions,
103+
);
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { describe, it, beforeAll } from "vitest";
2+
import type { Address } from "@solana/kit";
3+
import { assertAccountExists } from "@solana/kit";
4+
import { transferLockedPositionInstructions } from "../src/transferLockedPosition";
5+
import { rpc, sendTransaction, signer } from "./utils/mockRpc";
6+
import { setDefaultFunder } from "../src/config";
7+
import assert from "assert";
8+
import { setupAta, setupMint } from "./utils/token";
9+
import {
10+
setupAtaTE,
11+
setupMintTE,
12+
setupMintTEFee,
13+
} from "./utils/tokenExtensions";
14+
import { setupWhirlpool } from "./utils/program";
15+
import { openPositionInstructions } from "../src/increaseLiquidity";
16+
import {
17+
fetchLockConfig,
18+
getPositionAddress,
19+
LockType,
20+
} from "@orca-so/whirlpools-client";
21+
import { lockPositionInstructions } from "../src/lockPosition";
22+
import { getLockConfigAddress } from "../../client/src/pda/lockConfig";
23+
import {
24+
fetchMaybeToken,
25+
findAssociatedTokenPda,
26+
TOKEN_2022_PROGRAM_ADDRESS,
27+
} from "@solana-program/token-2022";
28+
import { getNextKeypair } from "./utils/keypair";
29+
30+
const mintTypes = new Map([
31+
["A", setupMint],
32+
["B", setupMint],
33+
["TEA", setupMintTE],
34+
["TEB", setupMintTE],
35+
["TEFee", setupMintTEFee],
36+
]);
37+
38+
const ataTypes = new Map([
39+
["A", setupAta],
40+
["B", setupAta],
41+
["TEA", setupAtaTE],
42+
["TEB", setupAtaTE],
43+
["TEFee", setupAtaTE],
44+
]);
45+
46+
const poolTypes = new Map([
47+
["A-B", setupWhirlpool],
48+
["A-TEA", setupWhirlpool],
49+
["TEA-TEB", setupWhirlpool],
50+
["A-TEFee", setupWhirlpool],
51+
]);
52+
53+
describe("Create TransferLockedPosition instructions", () => {
54+
const tickSpacing = 64;
55+
const tokenBalance = 1_000_000n;
56+
const mints: Map<string, Address> = new Map();
57+
const atas: Map<string, Address> = new Map();
58+
const pools: Map<string, Address> = new Map();
59+
60+
beforeAll(async () => {
61+
for (const [name, setup] of mintTypes) {
62+
mints.set(name, await setup());
63+
}
64+
65+
for (const [name, setup] of ataTypes) {
66+
const mint = mints.get(name)!;
67+
atas.set(name, await setup(mint, { amount: tokenBalance }));
68+
}
69+
70+
for (const [name, setup] of poolTypes) {
71+
const [mintAKey, mintBKey] = name.split("-");
72+
const mintA = mints.get(mintAKey)!;
73+
const mintB = mints.get(mintBKey)!;
74+
pools.set(name, await setup(mintA, mintB, tickSpacing));
75+
}
76+
});
77+
78+
const testTransferLockedPositionInstructions = async (
79+
poolName: string,
80+
lowerPrice: number,
81+
upperPrice: number,
82+
) => {
83+
const whirlpool = pools.get(poolName)!;
84+
const param = { liquidity: 10_000n };
85+
86+
// Position owner has position, can lock it, and transfer it.
87+
const positionOwner = signer;
88+
setDefaultFunder(positionOwner);
89+
90+
// Open position by position owner
91+
const { instructions, positionMint } = await openPositionInstructions(
92+
rpc,
93+
whirlpool,
94+
param,
95+
lowerPrice,
96+
upperPrice,
97+
);
98+
99+
const positionAddress = await getPositionAddress(positionMint);
100+
await sendTransaction(instructions);
101+
102+
// After creating a new position, owner locks it.
103+
const lockConfigPda = await getLockConfigAddress(positionAddress[0]);
104+
const positionAta = await findAssociatedTokenPda({
105+
mint: positionMint,
106+
owner: positionOwner.address,
107+
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS,
108+
});
109+
110+
const lockPositionIx = await lockPositionInstructions({
111+
lockType: LockType.Permanent,
112+
funder: positionOwner,
113+
positionAuthority: signer,
114+
position: positionAddress[0],
115+
positionMint: positionMint,
116+
positionTokenAccount: positionAta[0],
117+
lockConfigPda: lockConfigPda[0],
118+
whirlpool: whirlpool,
119+
});
120+
121+
await sendTransaction(lockPositionIx.instructions);
122+
123+
// Prepare a new ATA to transfer locked position.
124+
// This new ATA has same position mint, but different owner.
125+
const nextSigner = getNextKeypair();
126+
127+
const receiverAta = await setupAtaTE(positionMint, {
128+
amount: 0,
129+
signer: nextSigner,
130+
});
131+
132+
const transferLockedPositionIx = await transferLockedPositionInstructions(
133+
rpc,
134+
{
135+
position: positionAddress[0],
136+
positionMint: positionMint,
137+
positionTokenAccount: positionAta[0],
138+
detinationTokenAccount: receiverAta,
139+
lockConfig: lockConfigPda[0],
140+
positionAuthority: positionOwner.address,
141+
receiver: nextSigner.address,
142+
},
143+
positionOwner,
144+
);
145+
146+
await sendTransaction(transferLockedPositionIx.instructions);
147+
148+
// verify lock config is still permanent
149+
const lockConfig = await fetchLockConfig(rpc, lockConfigPda[0]);
150+
assert.strictEqual(lockConfig.data.lockType, LockType.Permanent);
151+
152+
// verify position is transferred
153+
const receiverAtaInfo = await fetchMaybeToken(rpc, receiverAta);
154+
assertAccountExists(receiverAtaInfo);
155+
};
156+
157+
for (const poolName of poolTypes.keys()) {
158+
it("Should not be able to transfer a position not owned by the signer", async () => {
159+
await testTransferLockedPositionInstructions(poolName, 0.95, 1.05);
160+
});
161+
}
162+
});

0 commit comments

Comments
 (0)