Skip to content

Commit f387724

Browse files
authored
feat: supporting unlocked with withdrawal period (#6)
The underlying codebase requires the user to decide at lockup the Date of withdrawal. The intended behaviour, is that the user can decide to lock the tokens in an "unlocked with withdrawal period" mode, in which it would require the user to first "initiate the withdrawal", to be able to actually withdraw the tokens (e.g.) 7 days later. This is achieved by: - reverting on withdrawal when locking Date is 0 - adding a new method, identical in arguments to "unlock", that would revert if Date != 0 - when user calls this method, the locking Date should be `now() + 7 days`
1 parent abec526 commit f387724

File tree

9 files changed

+553
-187
lines changed

9 files changed

+553
-187
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ js/lib
77

88
/node_modules
99

10-
js/dist
10+
js/dist
11+
**/wallet.json

js/src/example.ts

Lines changed: 10 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -20,32 +20,7 @@ const wallet = Keypair.fromSecretKey(
2020
);
2121

2222
/** There are better way to generate an array of dates but be careful as it's irreversible */
23-
const DATES = [
24-
new Date(2022, 12),
25-
new Date(2023, 1),
26-
new Date(2023, 2),
27-
new Date(2023, 3),
28-
new Date(2023, 4),
29-
new Date(2023, 5),
30-
new Date(2023, 6),
31-
new Date(2023, 7),
32-
new Date(2023, 8),
33-
new Date(2023, 9),
34-
new Date(2023, 10),
35-
new Date(2023, 11),
36-
new Date(2024, 12),
37-
new Date(2024, 2),
38-
new Date(2024, 3),
39-
new Date(2024, 4),
40-
new Date(2024, 5),
41-
new Date(2024, 6),
42-
new Date(2024, 7),
43-
new Date(2024, 8),
44-
new Date(2024, 9),
45-
new Date(2024, 10),
46-
new Date(2024, 11),
47-
new Date(2024, 12),
48-
];
23+
const DATE = new Date(2022, 12);
4924

5025
/** Info about the desintation */
5126
const DESTINATION_OWNER = new PublicKey('');
@@ -86,19 +61,14 @@ const checks = async () => {
8661
/** Function that locks the tokens */
8762
const lock = async () => {
8863
await checks();
89-
const schedules: Schedule[] = [];
90-
for (let date of DATES) {
91-
schedules.push(
92-
new Schedule(
93-
/** Has to be in seconds */
94-
// @ts-ignore
95-
new Numberu64(date.getTime() / 1_000),
96-
/** Don't forget to add decimals */
97-
// @ts-ignore
98-
new Numberu64(AMOUNT_PER_SCHEDULE * Math.pow(10, DECIMALS)),
99-
),
100-
);
101-
}
64+
const schedule: Schedule = new Schedule(
65+
/** Has to be in seconds */
66+
// @ts-ignore
67+
new Numberu64(DATE.getTime() / 1_000),
68+
/** Don't forget to add decimals */
69+
// @ts-ignore
70+
new Numberu64(AMOUNT_PER_SCHEDULE * Math.pow(10, DECIMALS)),
71+
);
10272
const seed = generateRandomSeed();
10373

10474
console.log(`Seed: ${seed}`);
@@ -112,7 +82,7 @@ const lock = async () => {
11282
SOURCE_TOKEN_ACCOUNT,
11383
DESTINATION_TOKEN_ACCOUNT,
11484
MINT,
115-
schedules,
85+
schedule,
11686
);
11787

11888
const tx = await signAndSendInstructions(connection, [], wallet, instruction);

js/src/instructions.ts

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,11 @@ export function createInitInstruction(
1212
vestingProgramId: PublicKey,
1313
payerKey: PublicKey,
1414
vestingAccountKey: PublicKey,
15-
seeds: Array<Buffer | Uint8Array>,
16-
numberOfSchedules: number,
15+
seeds: Array<Buffer | Uint8Array>
1716
): TransactionInstruction {
1817
let buffers = [
1918
Buffer.from(Int8Array.from([0]).buffer),
2019
Buffer.concat(seeds),
21-
// @ts-ignore
22-
new Numberu32(numberOfSchedules).toBuffer(),
2320
];
2421

2522
const data = Buffer.concat(buffers);
@@ -62,7 +59,7 @@ export function createCreateInstruction(
6259
sourceTokenAccountKey: PublicKey,
6360
destinationTokenAccountKey: PublicKey,
6461
mintAddress: PublicKey,
65-
schedules: Array<Schedule>,
62+
schedule: Schedule,
6663
seeds: Array<Buffer | Uint8Array>,
6764
): TransactionInstruction {
6865
let buffers = [
@@ -72,9 +69,7 @@ export function createCreateInstruction(
7269
destinationTokenAccountKey.toBuffer(),
7370
];
7471

75-
schedules.forEach(s => {
76-
buffers.push(s.toBuffer());
77-
});
72+
buffers.push(schedule.toBuffer());
7873

7974
const data = Buffer.concat(buffers);
8075
const keys = [
@@ -158,3 +153,51 @@ export function createUnlockInstruction(
158153
data,
159154
});
160155
}
156+
157+
export function createInitializeUnlockInstruction(
158+
vestingProgramId: PublicKey,
159+
tokenProgramId: PublicKey,
160+
clockSysvarId: PublicKey,
161+
vestingAccountKey: PublicKey,
162+
vestingTokenAccountKey: PublicKey,
163+
destinationTokenAccountKey: PublicKey,
164+
seeds: Array<Buffer | Uint8Array>,
165+
): TransactionInstruction {
166+
const data = Buffer.concat([
167+
Buffer.from(Int8Array.from([3]).buffer),
168+
Buffer.concat(seeds),
169+
]);
170+
171+
const keys = [
172+
{
173+
pubkey: tokenProgramId,
174+
isSigner: false,
175+
isWritable: false,
176+
},
177+
{
178+
pubkey: clockSysvarId,
179+
isSigner: false,
180+
isWritable: false,
181+
},
182+
{
183+
pubkey: vestingAccountKey,
184+
isSigner: false,
185+
isWritable: true,
186+
},
187+
{
188+
pubkey: vestingTokenAccountKey,
189+
isSigner: false,
190+
isWritable: true,
191+
},
192+
{
193+
pubkey: destinationTokenAccountKey,
194+
isSigner: false,
195+
isWritable: true,
196+
},
197+
];
198+
return new TransactionInstruction({
199+
keys,
200+
programId: vestingProgramId,
201+
data,
202+
});
203+
}

js/src/main.ts

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
createCreateInstruction,
1515
createInitInstruction,
1616
createUnlockInstruction,
17+
createInitializeUnlockInstruction,
1718
} from './instructions';
1819
import { ContractInfo, Schedule } from './state';
1920
import { assert } from 'console';
@@ -36,7 +37,7 @@ export const TOKEN_VESTING_PROGRAM_ID = new PublicKey(
3637
* @param possibleSourceTokenPubkey The source token account (i.e where locked tokens are originating from), if null it defaults to the ATA
3738
* @param destinationTokenPubkey The destination token account i.e where unlocked tokens will be transfered
3839
* @param mintAddress The mint of the tokens being vested
39-
* @param schedules The array of vesting schedules
40+
* @param schedule The vesting schedule
4041
* @returns An array of `TransactionInstruction`
4142
*/
4243
export async function create(
@@ -48,7 +49,7 @@ export async function create(
4849
possibleSourceTokenPubkey: PublicKey | null,
4950
destinationTokenPubkey: PublicKey,
5051
mintAddress: PublicKey,
51-
schedules: Array<Schedule>,
52+
schedule: Schedule,
5253
): Promise<Array<TransactionInstruction>> {
5354
// If no source token account was given, use the associated source account
5455
if (possibleSourceTokenPubkey == null) {
@@ -92,8 +93,7 @@ export async function create(
9293
programId,
9394
payer,
9495
vestingAccountKey,
95-
[seedWord],
96-
schedules.length,
96+
[seedWord]
9797
),
9898
createAssociatedTokenAccountInstruction(
9999
payer,
@@ -110,7 +110,7 @@ export async function create(
110110
possibleSourceTokenPubkey,
111111
destinationTokenPubkey,
112112
mintAddress,
113-
schedules,
113+
schedule,
114114
[seedWord],
115115
),
116116
];
@@ -161,6 +161,50 @@ export async function unlock(
161161
return instruction;
162162
}
163163

164+
/**
165+
* This function can be used to initialize the unlock of vested tokens
166+
* @param connection The Solana RPC connection object
167+
* @param programId The token vesting program ID
168+
* @param seedWord Seed words used to derive the vesting account
169+
* @param mintAddress The mint of the vested tokens
170+
* @returns An array of `TransactionInstruction`
171+
*/
172+
export async function initializeUnlock(
173+
connection: Connection,
174+
programId: PublicKey,
175+
seedWord: Buffer | Uint8Array,
176+
mintAddress: PublicKey,
177+
): Promise<Array<TransactionInstruction>> {
178+
seedWord = seedWord.slice(0, 31);
179+
const [vestingAccountKey, bump] = await PublicKey.findProgramAddress(
180+
[seedWord],
181+
programId,
182+
);
183+
seedWord = Buffer.from(seedWord.toString('hex') + bump.toString(16), 'hex');
184+
185+
const vestingTokenAccountKey = await getAssociatedTokenAddress(
186+
mintAddress,
187+
vestingAccountKey,
188+
true,
189+
);
190+
191+
const vestingInfo = await getContractInfo(connection, vestingAccountKey);
192+
193+
let instruction = [
194+
createInitializeUnlockInstruction(
195+
programId,
196+
TOKEN_PROGRAM_ID,
197+
SYSVAR_CLOCK_PUBKEY,
198+
vestingAccountKey,
199+
vestingTokenAccountKey,
200+
vestingInfo.destinationAddress,
201+
[seedWord],
202+
),
203+
];
204+
205+
return instruction;
206+
}
207+
164208
/**
165209
* This function can be used retrieve information about a vesting account
166210
* @param connection The Solana RPC connection object

program/Cargo.lock

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

program/Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ arbitrary = { version = "0.4", features = ["derive"], optional = true }
2828
honggfuzz = { version = "0.5", optional = true }
2929

3030
[dev-dependencies]
31-
solana-sdk = "1.5.6"
32-
solana-program-test = "1.5.6"
31+
solana-sdk = "1.18.23"
32+
solana-program-test = "1.18.23"
33+
solana-test-framework = { git = "https://github.com/halbornteam/solana-test-framework", branch = "solana1.18" }
3334
tokio = { version = "1.0", features = ["macros"]}
3435

3536
[lib]

0 commit comments

Comments
 (0)