@carrot-protocol/clend-vaults-rpc 0.0.2-pub1-dev-a741874
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierignore +2 -0
- package/makefile +18 -0
- package/package.json +26 -0
- package/src/addresses.ts +19 -0
- package/src/idl/clend_vaults.json +1550 -0
- package/src/idl/clend_vaults.ts +1556 -0
- package/src/index.ts +5 -0
- package/src/math.ts +103 -0
- package/src/program.ts +551 -0
- package/src/rpc.ts +1354 -0
- package/src/state.ts +188 -0
- package/src/swapper.ts +221 -0
- package/src/utils.ts +7 -0
- package/tsconfig.json +18 -0
package/src/rpc.ts
ADDED
|
@@ -0,0 +1,1354 @@
|
|
|
1
|
+
// rpc.ts
|
|
2
|
+
import { AnchorProvider, BN, Program, web3 } from "@coral-xyz/anchor";
|
|
3
|
+
import { Connection } from "@solana/web3.js";
|
|
4
|
+
import {
|
|
5
|
+
AuthorityType,
|
|
6
|
+
createInitializeInstruction,
|
|
7
|
+
createSetAuthorityInstruction,
|
|
8
|
+
getAssociatedTokenAddressSync,
|
|
9
|
+
MintLayout,
|
|
10
|
+
RawMint,
|
|
11
|
+
TOKEN_2022_PROGRAM_ID,
|
|
12
|
+
TOKEN_PROGRAM_ID,
|
|
13
|
+
unpackMint,
|
|
14
|
+
} from "@solana/spl-token";
|
|
15
|
+
import { Decimal } from "decimal.js";
|
|
16
|
+
|
|
17
|
+
import { AddAssetArgs, ClendVaultsProgram } from "./program";
|
|
18
|
+
import {
|
|
19
|
+
CLEND_VAULTS_PROGRAM_ID,
|
|
20
|
+
getVaultPda,
|
|
21
|
+
JUPITER_SWAP_PROGRAM_ID,
|
|
22
|
+
} from "./addresses";
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
getBankPda,
|
|
26
|
+
getBankLiquidityVaultPda,
|
|
27
|
+
getBankLiquidityVaultAuthorityPda,
|
|
28
|
+
CLEND_PROGRAM_ID,
|
|
29
|
+
getClendAccountRemainingAccounts,
|
|
30
|
+
ClendClient,
|
|
31
|
+
getClendAccountActiveBanks,
|
|
32
|
+
Bank,
|
|
33
|
+
ISwapper,
|
|
34
|
+
getTokenProgramForMint,
|
|
35
|
+
getTokenProgramForMintFromRpc,
|
|
36
|
+
BankFlags,
|
|
37
|
+
} from "@carrot-protocol/clend-rpc";
|
|
38
|
+
import {
|
|
39
|
+
Vault,
|
|
40
|
+
VaultAsset,
|
|
41
|
+
VaultAssetReserve,
|
|
42
|
+
VaultClendAccount,
|
|
43
|
+
VaultEquity,
|
|
44
|
+
parseVaultAccount,
|
|
45
|
+
} from "./state";
|
|
46
|
+
import { ClendVaultsJupSwapper } from "./swapper";
|
|
47
|
+
import {
|
|
48
|
+
amountToUi,
|
|
49
|
+
calculateLendingAccountEquity,
|
|
50
|
+
calculateVaultNav,
|
|
51
|
+
convertSharesToAsset,
|
|
52
|
+
uiToAmount,
|
|
53
|
+
} from "./math";
|
|
54
|
+
|
|
55
|
+
export type Wallet = AnchorProvider["wallet"];
|
|
56
|
+
type AccountMeta = web3.AccountMeta;
|
|
57
|
+
|
|
58
|
+
export class ClendVaultsClient {
|
|
59
|
+
readonly connection: Connection;
|
|
60
|
+
readonly program: ClendVaultsProgram;
|
|
61
|
+
readonly clendClient: ClendClient;
|
|
62
|
+
readonly swapper: ISwapper;
|
|
63
|
+
readonly skipPreflight: boolean;
|
|
64
|
+
|
|
65
|
+
constructor(
|
|
66
|
+
provider: AnchorProvider,
|
|
67
|
+
skipPreflight: boolean = false,
|
|
68
|
+
swapperOverride?: ISwapper,
|
|
69
|
+
) {
|
|
70
|
+
this.connection = provider.connection;
|
|
71
|
+
this.program = new ClendVaultsProgram(provider);
|
|
72
|
+
this.skipPreflight = skipPreflight;
|
|
73
|
+
this.clendClient = new ClendClient(
|
|
74
|
+
provider.connection,
|
|
75
|
+
provider.wallet as any,
|
|
76
|
+
skipPreflight,
|
|
77
|
+
0,
|
|
78
|
+
);
|
|
79
|
+
this.swapper =
|
|
80
|
+
swapperOverride || new ClendVaultsJupSwapper(this.connection);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
address(): web3.PublicKey {
|
|
84
|
+
return this.program.program.provider.publicKey!;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async getVault(vault: web3.PublicKey): Promise<Vault> {
|
|
88
|
+
const vaultAccountInfo = await this.connection.getAccountInfo(vault);
|
|
89
|
+
if (!vaultAccountInfo) {
|
|
90
|
+
throw new Error(`Vault account not found: ${vault.toString()}`);
|
|
91
|
+
}
|
|
92
|
+
return parseVaultAccount(vault, vaultAccountInfo, this.connection);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async getVaultEquity(vault: Vault): Promise<VaultEquity> {
|
|
96
|
+
let vaultEquity = 0;
|
|
97
|
+
const clendAccountEquity: { address: web3.PublicKey; equity: number }[] =
|
|
98
|
+
[];
|
|
99
|
+
|
|
100
|
+
// calculate equity for each clend account
|
|
101
|
+
for (const clendAccount of vault.clendAccounts) {
|
|
102
|
+
const clendAccountData = await this.clendClient.getClendAccount(
|
|
103
|
+
clendAccount.address,
|
|
104
|
+
);
|
|
105
|
+
if (!clendAccountData) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Clend account not found: ${clendAccount.address.toString()}`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const lendingAccountEquity = calculateLendingAccountEquity(
|
|
112
|
+
clendAccountData.lendingAccount,
|
|
113
|
+
);
|
|
114
|
+
clendAccountEquity.push({
|
|
115
|
+
address: clendAccount.address,
|
|
116
|
+
equity: lendingAccountEquity,
|
|
117
|
+
});
|
|
118
|
+
vaultEquity += lendingAccountEquity;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// calculate equity for each reserve
|
|
122
|
+
const reserveEquity: { address: web3.PublicKey; equity: number }[] = [];
|
|
123
|
+
for (const asset of vault.assets) {
|
|
124
|
+
const mint = asset.mint.address;
|
|
125
|
+
const priceUi = await this.clendClient.getPythOraclePrice(mint);
|
|
126
|
+
const rEquity = priceUi * asset.reserve.amountUi;
|
|
127
|
+
reserveEquity.push({ address: asset.reserve.address, equity: rEquity });
|
|
128
|
+
vaultEquity += rEquity;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
address: vault.address,
|
|
133
|
+
vaultEquity,
|
|
134
|
+
clendAccountEquity,
|
|
135
|
+
reserveEquity,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async initVault(
|
|
140
|
+
sharesMint: web3.PublicKey,
|
|
141
|
+
managementFeeBps: number,
|
|
142
|
+
): Promise<{ txSig: string; vault: web3.PublicKey }> {
|
|
143
|
+
const { vault, ixns: initVaultIxns } = await this.program.initVault(
|
|
144
|
+
sharesMint,
|
|
145
|
+
this.address(),
|
|
146
|
+
managementFeeBps,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const initializeTxSig = await this.send(initVaultIxns);
|
|
150
|
+
|
|
151
|
+
return { txSig: initializeTxSig, vault };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async addAsset(
|
|
155
|
+
vault: web3.PublicKey,
|
|
156
|
+
assetMint: web3.PublicKey,
|
|
157
|
+
assetOracle: web3.PublicKey,
|
|
158
|
+
addAssetArgs: AddAssetArgs,
|
|
159
|
+
): Promise<string> {
|
|
160
|
+
const ixns = await this.program.addAsset(
|
|
161
|
+
vault,
|
|
162
|
+
this.address(),
|
|
163
|
+
assetMint,
|
|
164
|
+
assetOracle,
|
|
165
|
+
TOKEN_PROGRAM_ID,
|
|
166
|
+
addAssetArgs,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const txSig = await this.send(ixns);
|
|
170
|
+
|
|
171
|
+
return txSig;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async addClendAccount(
|
|
175
|
+
vault: web3.PublicKey,
|
|
176
|
+
clendGroup: web3.PublicKey,
|
|
177
|
+
): Promise<{ txSig: string; clendAccount: web3.PublicKey }> {
|
|
178
|
+
const clendAccount = web3.Keypair.generate();
|
|
179
|
+
|
|
180
|
+
const addClendAccountIxns = await this.program.addClendAccount(
|
|
181
|
+
vault,
|
|
182
|
+
this.address(),
|
|
183
|
+
clendGroup,
|
|
184
|
+
clendAccount,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const txSig = await this.send([...addClendAccountIxns], [clendAccount]);
|
|
188
|
+
|
|
189
|
+
return { txSig, clendAccount: clendAccount.publicKey };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async updateVaultManager(
|
|
193
|
+
vault: web3.PublicKey,
|
|
194
|
+
newManager: web3.PublicKey,
|
|
195
|
+
): Promise<web3.TransactionInstruction[]> {
|
|
196
|
+
return this.program.updateVaultManager(vault, this.address(), newManager);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async issue(
|
|
200
|
+
vault: web3.PublicKey,
|
|
201
|
+
assetMint: web3.PublicKey,
|
|
202
|
+
amount: BN,
|
|
203
|
+
): Promise<string> {
|
|
204
|
+
const ixns = await this.prepareIssueIxns(
|
|
205
|
+
this.address(),
|
|
206
|
+
vault,
|
|
207
|
+
assetMint,
|
|
208
|
+
amount,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const txSig = await this.send(ixns);
|
|
212
|
+
|
|
213
|
+
return txSig;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async prepareIssueIxns(
|
|
217
|
+
user: web3.PublicKey,
|
|
218
|
+
vault: web3.PublicKey,
|
|
219
|
+
assetMint: web3.PublicKey,
|
|
220
|
+
amount: BN,
|
|
221
|
+
): Promise<web3.TransactionInstruction[]> {
|
|
222
|
+
// get vault and asset data
|
|
223
|
+
const vaultData = await this.getVault(vault);
|
|
224
|
+
const assetData = vaultData.assets.find((a) =>
|
|
225
|
+
a.mint.address.equals(assetMint),
|
|
226
|
+
)!;
|
|
227
|
+
|
|
228
|
+
const remainingAccounts = await buildEquityRemainingAccounts(
|
|
229
|
+
vaultData,
|
|
230
|
+
this.clendClient,
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const ixns = await this.program.issue(
|
|
234
|
+
vault,
|
|
235
|
+
vaultData.sharesMint,
|
|
236
|
+
user,
|
|
237
|
+
assetMint,
|
|
238
|
+
assetData.oracle,
|
|
239
|
+
amount,
|
|
240
|
+
TOKEN_PROGRAM_ID,
|
|
241
|
+
TOKEN_2022_PROGRAM_ID,
|
|
242
|
+
remainingAccounts,
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
return ixns;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async redeem(
|
|
249
|
+
vault: web3.PublicKey,
|
|
250
|
+
assetMint: web3.PublicKey,
|
|
251
|
+
sharesAmount: BN,
|
|
252
|
+
): Promise<string> {
|
|
253
|
+
const ixns = await this.prepareRedeemIxns(
|
|
254
|
+
this.address(),
|
|
255
|
+
vault,
|
|
256
|
+
assetMint,
|
|
257
|
+
sharesAmount,
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const txSig = await this.send(ixns);
|
|
261
|
+
|
|
262
|
+
return txSig;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async prepareRedeemIxns(
|
|
266
|
+
user: web3.PublicKey,
|
|
267
|
+
vault: web3.PublicKey,
|
|
268
|
+
assetMint: web3.PublicKey,
|
|
269
|
+
amount: BN,
|
|
270
|
+
): Promise<web3.TransactionInstruction[]> {
|
|
271
|
+
const vaultData = await this.getVault(vault);
|
|
272
|
+
const assetData = vaultData.assets.find((a) =>
|
|
273
|
+
a.mint.address.equals(assetMint),
|
|
274
|
+
)!;
|
|
275
|
+
|
|
276
|
+
const remainingAccounts = await buildEquityRemainingAccounts(
|
|
277
|
+
vaultData,
|
|
278
|
+
this.clendClient,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
return this.program.redeem(
|
|
282
|
+
vault,
|
|
283
|
+
vaultData.sharesMint,
|
|
284
|
+
user,
|
|
285
|
+
assetMint,
|
|
286
|
+
assetData.oracle,
|
|
287
|
+
amount,
|
|
288
|
+
TOKEN_PROGRAM_ID,
|
|
289
|
+
TOKEN_2022_PROGRAM_ID,
|
|
290
|
+
remainingAccounts,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async clendAccountDeposit(
|
|
295
|
+
vault: web3.PublicKey,
|
|
296
|
+
clendAccount: web3.PublicKey,
|
|
297
|
+
assetMint: web3.PublicKey,
|
|
298
|
+
amount: BN,
|
|
299
|
+
): Promise<string> {
|
|
300
|
+
// fetch clend account data
|
|
301
|
+
const clendAccountData =
|
|
302
|
+
await this.clendClient.getClendAccount(clendAccount);
|
|
303
|
+
if (!clendAccountData) {
|
|
304
|
+
throw new Error(`Clend account not found: ${clendAccount.toString()}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// fetch vault data
|
|
308
|
+
const vaultData = await this.getVault(vault);
|
|
309
|
+
const assetData = vaultData.assets.find((a) =>
|
|
310
|
+
a.mint.address.equals(assetMint),
|
|
311
|
+
)!;
|
|
312
|
+
|
|
313
|
+
// remaining accounts for protocol call
|
|
314
|
+
const ra = getClendAccountRemainingAccounts([]);
|
|
315
|
+
|
|
316
|
+
const ixns = await this.program.clendAccountDeposit(
|
|
317
|
+
vault,
|
|
318
|
+
this.address(),
|
|
319
|
+
clendAccountData.group,
|
|
320
|
+
clendAccount,
|
|
321
|
+
assetMint,
|
|
322
|
+
TOKEN_PROGRAM_ID,
|
|
323
|
+
amount,
|
|
324
|
+
assetData.reserve.address,
|
|
325
|
+
ra,
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
const txSig = await this.send(ixns);
|
|
329
|
+
|
|
330
|
+
return txSig;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async clendAccountWithdraw(
|
|
334
|
+
vault: web3.PublicKey,
|
|
335
|
+
clendAccount: web3.PublicKey,
|
|
336
|
+
assetMint: web3.PublicKey,
|
|
337
|
+
amount: BN,
|
|
338
|
+
): Promise<string> {
|
|
339
|
+
// fetch clend account data
|
|
340
|
+
const clendAccountData =
|
|
341
|
+
await this.clendClient.getClendAccount(clendAccount);
|
|
342
|
+
if (!clendAccountData) {
|
|
343
|
+
throw new Error(`Clend account not found: ${clendAccount.toString()}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// fetch active banks
|
|
347
|
+
const clendAccountActiveBanks =
|
|
348
|
+
getClendAccountActiveBanks(clendAccountData);
|
|
349
|
+
|
|
350
|
+
const activeBankData: Bank[] = [];
|
|
351
|
+
for (const bank of clendAccountActiveBanks) {
|
|
352
|
+
const bankData = await this.clendClient.getBank(bank);
|
|
353
|
+
activeBankData.push(bankData);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const ra = getClendAccountRemainingAccounts(activeBankData);
|
|
357
|
+
|
|
358
|
+
const ixns = await this.program.clendAccountWithdraw(
|
|
359
|
+
vault,
|
|
360
|
+
this.address(),
|
|
361
|
+
clendAccountData.group,
|
|
362
|
+
clendAccount,
|
|
363
|
+
assetMint,
|
|
364
|
+
TOKEN_PROGRAM_ID,
|
|
365
|
+
amount,
|
|
366
|
+
ra,
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
const txSig = await this.send(ixns);
|
|
370
|
+
|
|
371
|
+
return txSig;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async clendAccountRepay(
|
|
375
|
+
vault: web3.PublicKey,
|
|
376
|
+
clendAccount: web3.PublicKey,
|
|
377
|
+
assetMint: web3.PublicKey,
|
|
378
|
+
amount: BN | number,
|
|
379
|
+
): Promise<string> {
|
|
380
|
+
const clendAccountData =
|
|
381
|
+
await this.clendClient.getClendAccount(clendAccount);
|
|
382
|
+
if (!clendAccountData) {
|
|
383
|
+
throw new Error(`Clend account not found: ${clendAccount.toString()}`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const ra = getClendAccountRemainingAccounts([]);
|
|
387
|
+
|
|
388
|
+
const ixns = await this.program.clendAccountRepay(
|
|
389
|
+
vault,
|
|
390
|
+
this.address(),
|
|
391
|
+
clendAccountData.group,
|
|
392
|
+
clendAccount,
|
|
393
|
+
assetMint,
|
|
394
|
+
TOKEN_PROGRAM_ID,
|
|
395
|
+
amount,
|
|
396
|
+
ra,
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
const txSig = await this.send(ixns);
|
|
400
|
+
|
|
401
|
+
return txSig;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async clendAccountBorrow(
|
|
405
|
+
vault: web3.PublicKey,
|
|
406
|
+
clendAccount: web3.PublicKey,
|
|
407
|
+
assetMint: web3.PublicKey,
|
|
408
|
+
amount: BN,
|
|
409
|
+
): Promise<string> {
|
|
410
|
+
const clendAccountData =
|
|
411
|
+
await this.clendClient.getClendAccount(clendAccount);
|
|
412
|
+
if (!clendAccountData) {
|
|
413
|
+
throw new Error(`Clend account not found: ${clendAccount.toString()}`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// fetch active banks
|
|
417
|
+
const clendAccountActiveBanks =
|
|
418
|
+
getClendAccountActiveBanks(clendAccountData);
|
|
419
|
+
|
|
420
|
+
// add target bank if not already in list
|
|
421
|
+
// this would be the case for a first time borrow
|
|
422
|
+
const targetBank = getBankPda(clendAccountData.group, assetMint);
|
|
423
|
+
const alreadyInBankList = clendAccountActiveBanks.some((c) =>
|
|
424
|
+
c.equals(targetBank),
|
|
425
|
+
);
|
|
426
|
+
if (!alreadyInBankList) {
|
|
427
|
+
clendAccountActiveBanks.push(targetBank);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const activeBankData: Bank[] = [];
|
|
431
|
+
for (const bank of clendAccountActiveBanks) {
|
|
432
|
+
const bankData = await this.clendClient.getBank(bank);
|
|
433
|
+
activeBankData.push(bankData);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const ra = getClendAccountRemainingAccounts(activeBankData);
|
|
437
|
+
|
|
438
|
+
const ixns = await this.program.clendAccountBorrow(
|
|
439
|
+
vault,
|
|
440
|
+
this.address(),
|
|
441
|
+
clendAccountData.group,
|
|
442
|
+
clendAccount,
|
|
443
|
+
assetMint,
|
|
444
|
+
TOKEN_PROGRAM_ID,
|
|
445
|
+
amount,
|
|
446
|
+
ra,
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
const txSig = await this.send(ixns);
|
|
450
|
+
|
|
451
|
+
return txSig;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async clendAccountClaimEmissions(
|
|
455
|
+
vault: web3.PublicKey,
|
|
456
|
+
clendGroup: web3.PublicKey,
|
|
457
|
+
clendAccount: web3.PublicKey,
|
|
458
|
+
bank: web3.PublicKey,
|
|
459
|
+
assetMint: web3.PublicKey,
|
|
460
|
+
emissionsMint: web3.PublicKey,
|
|
461
|
+
): Promise<web3.TransactionInstruction[]> {
|
|
462
|
+
const emissionsVault = getBankLiquidityVaultPda(bank);
|
|
463
|
+
const emissionsAuthority = getBankLiquidityVaultAuthorityPda(bank);
|
|
464
|
+
|
|
465
|
+
// destination is an ATA owned by the vault PDA
|
|
466
|
+
const destinationAccount = getAssociatedTokenAddressSync(
|
|
467
|
+
emissionsMint,
|
|
468
|
+
vault,
|
|
469
|
+
true,
|
|
470
|
+
TOKEN_PROGRAM_ID,
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
return this.program.clendAccountClaimEmissions(
|
|
474
|
+
vault,
|
|
475
|
+
this.address(),
|
|
476
|
+
clendGroup,
|
|
477
|
+
clendAccount,
|
|
478
|
+
bank,
|
|
479
|
+
assetMint,
|
|
480
|
+
emissionsMint,
|
|
481
|
+
destinationAccount,
|
|
482
|
+
TOKEN_PROGRAM_ID,
|
|
483
|
+
emissionsVault,
|
|
484
|
+
emissionsAuthority,
|
|
485
|
+
CLEND_PROGRAM_ID,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// adjust the leverage of the clend account
|
|
490
|
+
async clendAccountAdjustLeverage(
|
|
491
|
+
clendAccount: web3.PublicKey,
|
|
492
|
+
collateralMint: web3.PublicKey,
|
|
493
|
+
debtMint: web3.PublicKey,
|
|
494
|
+
targetLeverage: number,
|
|
495
|
+
slippageBps: number,
|
|
496
|
+
): Promise<string> {
|
|
497
|
+
const clendAccountState =
|
|
498
|
+
await this.clendClient.getClendAccount(clendAccount);
|
|
499
|
+
if (!clendAccountState) {
|
|
500
|
+
throw new Error(`Clend account not found: ${clendAccount.toString()}`);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const adjustLeverageTxSig = await this.clendClient.adjustLeverage(
|
|
504
|
+
clendAccountState.group,
|
|
505
|
+
clendAccount,
|
|
506
|
+
collateralMint,
|
|
507
|
+
debtMint,
|
|
508
|
+
targetLeverage,
|
|
509
|
+
slippageBps,
|
|
510
|
+
this.swapper,
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
return adjustLeverageTxSig;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// create instructions for redeeming shares
|
|
517
|
+
// however this will pull from a clend account balance
|
|
518
|
+
// the equity value of the shares desired for redemption
|
|
519
|
+
async prepareRedeemFromClendAccountIxns(
|
|
520
|
+
user: web3.PublicKey,
|
|
521
|
+
vault: web3.PublicKey,
|
|
522
|
+
clendAccount: web3.PublicKey,
|
|
523
|
+
outputAssetMint: web3.PublicKey,
|
|
524
|
+
collateralMint: web3.PublicKey,
|
|
525
|
+
debtMint: web3.PublicKey,
|
|
526
|
+
totalSharesToRedeem: BN,
|
|
527
|
+
sharesToRedeemFromClendAccount: BN,
|
|
528
|
+
slippageBps: number,
|
|
529
|
+
): Promise<{ ixns: web3.TransactionInstruction[]; luts: web3.PublicKey[] }> {
|
|
530
|
+
const vaultState = await this.getVault(vault);
|
|
531
|
+
const outputAssetState = vaultState.assets.find((a) =>
|
|
532
|
+
a.mint.address.equals(outputAssetMint),
|
|
533
|
+
);
|
|
534
|
+
if (!outputAssetState) {
|
|
535
|
+
throw new Error(
|
|
536
|
+
`output asset mint ${outputAssetMint.toString()} not found in vault ${vault.toString()}`,
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// get price of output asset
|
|
541
|
+
const priceUi = await this.clendClient.getPythOraclePrice(
|
|
542
|
+
outputAssetState.mint.address,
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
const vaultEquity = await this.getVaultEquity(vaultState);
|
|
546
|
+
|
|
547
|
+
const sharesSupplyUi = amountToUi(
|
|
548
|
+
vaultState.sharesSupply,
|
|
549
|
+
vaultState.sharesDecimals,
|
|
550
|
+
);
|
|
551
|
+
const sharesToRedeemFromClendAccountUi = amountToUi(
|
|
552
|
+
sharesToRedeemFromClendAccount,
|
|
553
|
+
vaultState.sharesDecimals,
|
|
554
|
+
);
|
|
555
|
+
const vaultNav = calculateVaultNav(vaultEquity.vaultEquity, sharesSupplyUi);
|
|
556
|
+
|
|
557
|
+
// TODO: will need to add fees to this
|
|
558
|
+
const desiredOutputAmount = convertSharesToAsset(
|
|
559
|
+
vaultNav,
|
|
560
|
+
sharesToRedeemFromClendAccountUi,
|
|
561
|
+
outputAssetState.mint.decimals,
|
|
562
|
+
priceUi,
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
// check if clend account is listed in vault config
|
|
566
|
+
const clendAccountIsValid = vaultState.clendAccounts.some((c) =>
|
|
567
|
+
c.address.equals(clendAccount),
|
|
568
|
+
);
|
|
569
|
+
if (!clendAccountIsValid) {
|
|
570
|
+
throw new Error(
|
|
571
|
+
`Clend account ${clendAccount.toString()} not found in vault ${vault.toString()} config`,
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// fetch clend account state
|
|
576
|
+
const clendAccountState =
|
|
577
|
+
await this.clendClient.getClendAccount(clendAccount);
|
|
578
|
+
if (!clendAccountState) {
|
|
579
|
+
throw new Error(
|
|
580
|
+
`error fetching clend account state: ${clendAccount.toString()}`,
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// we need to know how many ixns will go before the leverage ixns
|
|
585
|
+
// this is for the flash loan index positioning
|
|
586
|
+
let prependedIxCount: number = 0;
|
|
587
|
+
let cuIxCount: number = 0; // TODO: in the clend libs we already account for this
|
|
588
|
+
prependedIxCount += cuIxCount;
|
|
589
|
+
|
|
590
|
+
// get the redemption ixns
|
|
591
|
+
// these are run after the withdraw leverage ixns
|
|
592
|
+
const redeemIxns = await this.prepareRedeemIxns(
|
|
593
|
+
user,
|
|
594
|
+
vault,
|
|
595
|
+
outputAssetMint,
|
|
596
|
+
totalSharesToRedeem,
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
// calculate parameters for withdraw leverage
|
|
600
|
+
// based on output asset mint
|
|
601
|
+
let debtToRepay: BN;
|
|
602
|
+
let collateralToWithdraw: BN;
|
|
603
|
+
let debtBankData: Bank;
|
|
604
|
+
let collateralBankData: Bank;
|
|
605
|
+
switch (outputAssetMint.toString()) {
|
|
606
|
+
case collateralMint.toString():
|
|
607
|
+
// get params for withdraw leverage
|
|
608
|
+
const {
|
|
609
|
+
debtToRepay: debtToRepayCollateral,
|
|
610
|
+
collateralToWithdraw: collateralToWithdrawCollateral,
|
|
611
|
+
debtBankData: debtBankDataCollateral,
|
|
612
|
+
collateralBankData: collateralBankDataCollateral,
|
|
613
|
+
} = await this.clendClient.getNetWithdrawLeverageCollateralParams(
|
|
614
|
+
clendAccountState.group,
|
|
615
|
+
clendAccount,
|
|
616
|
+
collateralMint,
|
|
617
|
+
debtMint,
|
|
618
|
+
desiredOutputAmount,
|
|
619
|
+
false,
|
|
620
|
+
);
|
|
621
|
+
debtToRepay = debtToRepayCollateral;
|
|
622
|
+
collateralToWithdraw = collateralToWithdrawCollateral;
|
|
623
|
+
debtBankData = debtBankDataCollateral;
|
|
624
|
+
collateralBankData = collateralBankDataCollateral;
|
|
625
|
+
break;
|
|
626
|
+
case debtMint.toString():
|
|
627
|
+
// get params for withdraw leverage
|
|
628
|
+
const {
|
|
629
|
+
debtToRepay: debtToRepayDebt,
|
|
630
|
+
collateralToWithdraw: collateralToWithdrawDebt,
|
|
631
|
+
debtBankData: debtBankDataDebt,
|
|
632
|
+
collateralBankData: collateralBankDataDebt,
|
|
633
|
+
} = await this.clendClient.getNetWithdrawLeverageDebtParams(
|
|
634
|
+
clendAccountState.group,
|
|
635
|
+
clendAccount,
|
|
636
|
+
collateralMint,
|
|
637
|
+
debtMint,
|
|
638
|
+
desiredOutputAmount,
|
|
639
|
+
false,
|
|
640
|
+
);
|
|
641
|
+
debtToRepay = debtToRepayDebt;
|
|
642
|
+
collateralToWithdraw = collateralToWithdrawDebt;
|
|
643
|
+
debtBankData = debtBankDataDebt;
|
|
644
|
+
collateralBankData = collateralBankDataDebt;
|
|
645
|
+
break;
|
|
646
|
+
default:
|
|
647
|
+
throw new Error(
|
|
648
|
+
`invalid output token mint: ${outputAssetMint.toString()}`,
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// compose instructions for user to sign
|
|
653
|
+
let ixns: web3.TransactionInstruction[] = [];
|
|
654
|
+
let luts: web3.PublicKey[] = [];
|
|
655
|
+
switch (outputAssetMint.toString()) {
|
|
656
|
+
case collateralMint.toString():
|
|
657
|
+
const { ixns: collateralIxns, luts: collateralLuts } =
|
|
658
|
+
await this.getWithdrawLeverageCollateralIxns(
|
|
659
|
+
vault,
|
|
660
|
+
vaultState.manager,
|
|
661
|
+
user,
|
|
662
|
+
clendAccountState.group,
|
|
663
|
+
clendAccount,
|
|
664
|
+
collateralMint,
|
|
665
|
+
debtMint,
|
|
666
|
+
collateralBankData.mintDecimals,
|
|
667
|
+
debtBankData.mintDecimals,
|
|
668
|
+
false,
|
|
669
|
+
collateralToWithdraw,
|
|
670
|
+
desiredOutputAmount,
|
|
671
|
+
debtToRepay,
|
|
672
|
+
slippageBps,
|
|
673
|
+
prependedIxCount,
|
|
674
|
+
this.swapper,
|
|
675
|
+
);
|
|
676
|
+
ixns = collateralIxns;
|
|
677
|
+
luts = collateralLuts;
|
|
678
|
+
break;
|
|
679
|
+
case debtMint.toString():
|
|
680
|
+
const { ixns: debtIxns, luts: debtLuts } =
|
|
681
|
+
await this.getWithdrawLeverageDebtIxns(
|
|
682
|
+
vault,
|
|
683
|
+
vaultState.manager,
|
|
684
|
+
user,
|
|
685
|
+
clendAccountState.group,
|
|
686
|
+
clendAccount,
|
|
687
|
+
collateralMint,
|
|
688
|
+
debtMint,
|
|
689
|
+
collateralBankData.mintDecimals,
|
|
690
|
+
debtBankData.mintDecimals,
|
|
691
|
+
false,
|
|
692
|
+
debtToRepay,
|
|
693
|
+
collateralToWithdraw,
|
|
694
|
+
slippageBps,
|
|
695
|
+
prependedIxCount,
|
|
696
|
+
this.swapper,
|
|
697
|
+
);
|
|
698
|
+
ixns = debtIxns;
|
|
699
|
+
luts = debtLuts;
|
|
700
|
+
break;
|
|
701
|
+
default:
|
|
702
|
+
throw new Error(
|
|
703
|
+
`invalid output token mint: ${outputAssetMint.toString()}`,
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const allIxns: web3.TransactionInstruction[] = [...ixns, ...redeemIxns];
|
|
708
|
+
|
|
709
|
+
return { ixns: allIxns, luts };
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async getWithdrawLeverageCollateralIxns(
|
|
713
|
+
vault: web3.PublicKey,
|
|
714
|
+
vaultManager: web3.PublicKey,
|
|
715
|
+
user: web3.PublicKey,
|
|
716
|
+
clendGroup: web3.PublicKey,
|
|
717
|
+
clendAccount: web3.PublicKey,
|
|
718
|
+
collateralMint: web3.PublicKey,
|
|
719
|
+
debtMint: web3.PublicKey,
|
|
720
|
+
collateralDecimals: number,
|
|
721
|
+
debtDecimals: number,
|
|
722
|
+
withdrawAll: boolean,
|
|
723
|
+
collateralToWithdraw: BN,
|
|
724
|
+
desiredNetCollateralToReceive: BN,
|
|
725
|
+
debtToRepay: BN,
|
|
726
|
+
slippageBps: number,
|
|
727
|
+
additionalIxnCount: number,
|
|
728
|
+
swapperOverride?: ISwapper,
|
|
729
|
+
): Promise<{ ixns: web3.TransactionInstruction[]; luts: web3.PublicKey[] }> {
|
|
730
|
+
// override swapper if provided
|
|
731
|
+
let activeSwapper = this.swapper;
|
|
732
|
+
if (swapperOverride) {
|
|
733
|
+
activeSwapper = swapperOverride;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const clendAccountData =
|
|
737
|
+
await this.clendClient.getClendAccount(clendAccount);
|
|
738
|
+
if (!clendAccountData) {
|
|
739
|
+
throw new Error(`Clend account not found: ${clendAccount.toString()}`);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
let activeBanks = getClendAccountActiveBanks(clendAccountData);
|
|
743
|
+
const activeBankData = await this.clendClient.getBanks(activeBanks);
|
|
744
|
+
const remainingAccounts = getClendAccountRemainingAccounts(activeBankData);
|
|
745
|
+
|
|
746
|
+
// this is for partial withdraws
|
|
747
|
+
// overriden if withdrawAll is true
|
|
748
|
+
let collateralToSwap = collateralToWithdraw.sub(
|
|
749
|
+
desiredNetCollateralToReceive,
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
// again for partial withdraws
|
|
753
|
+
let swapQuote = await activeSwapper.getQuote({
|
|
754
|
+
payer: vault,
|
|
755
|
+
inputMint: collateralMint,
|
|
756
|
+
inputMintDecimals: collateralDecimals,
|
|
757
|
+
outputMint: debtMint,
|
|
758
|
+
outputMintDecimals: debtDecimals,
|
|
759
|
+
inputAmount: collateralToSwap,
|
|
760
|
+
slippageBps,
|
|
761
|
+
swapMode: "ExactIn",
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
if (withdrawAll) {
|
|
765
|
+
// Step 1: Get the current price via a nominal 1-token quote.
|
|
766
|
+
const oneToken = uiToAmount(1, collateralDecimals);
|
|
767
|
+
const nominalQuote = await activeSwapper.getQuote({
|
|
768
|
+
payer: vault,
|
|
769
|
+
inputMint: collateralMint,
|
|
770
|
+
inputMintDecimals: collateralDecimals,
|
|
771
|
+
outputMint: debtMint,
|
|
772
|
+
outputMintDecimals: debtDecimals,
|
|
773
|
+
inputAmount: oneToken,
|
|
774
|
+
slippageBps,
|
|
775
|
+
swapMode: "ExactIn",
|
|
776
|
+
});
|
|
777
|
+
const outAmountUi = amountToUi(
|
|
778
|
+
nominalQuote.otherAmountThreshold,
|
|
779
|
+
debtDecimals,
|
|
780
|
+
);
|
|
781
|
+
|
|
782
|
+
// Step 2: Estimate the ideal collateral needed to cover the debt.
|
|
783
|
+
const debtToRepayUi = new Decimal(
|
|
784
|
+
amountToUi(debtToRepay, debtDecimals).toString(),
|
|
785
|
+
);
|
|
786
|
+
const collateralToSwapUi_ideal = debtToRepayUi.div(outAmountUi);
|
|
787
|
+
const collateralToSwap_ideal = uiToAmount(
|
|
788
|
+
collateralToSwapUi_ideal.toNumber(),
|
|
789
|
+
collateralDecimals,
|
|
790
|
+
);
|
|
791
|
+
collateralToSwap = collateralToSwap_ideal;
|
|
792
|
+
//collateralToSwap = adjustAmountForSlippage(
|
|
793
|
+
// collateralToSwap_ideal,
|
|
794
|
+
// slippageBps,
|
|
795
|
+
//);
|
|
796
|
+
|
|
797
|
+
// Step 4 (Final Quote): Get the real quote for the buffered amount.
|
|
798
|
+
swapQuote = await activeSwapper.getQuote({
|
|
799
|
+
payer: user,
|
|
800
|
+
inputMint: collateralMint,
|
|
801
|
+
inputMintDecimals: collateralDecimals,
|
|
802
|
+
outputMint: debtMint,
|
|
803
|
+
outputMintDecimals: debtDecimals,
|
|
804
|
+
inputAmount: collateralToSwap,
|
|
805
|
+
slippageBps,
|
|
806
|
+
swapMode: "ExactIn",
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
// check its sufficient
|
|
810
|
+
if (swapQuote.outAmount.lt(debtToRepay)) {
|
|
811
|
+
throw new Error(
|
|
812
|
+
`Quote is insufficient to cover debt after slippage. Try increasing slippage tolerance.
|
|
813
|
+
${swapQuote.otherAmountThreshold.toString()} < ${debtToRepay.toString()}`,
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// 3. The final amount to repay is the minimum we are guaranteed to get from the swap.
|
|
819
|
+
const finalDebtToRepay = swapQuote.outAmount;
|
|
820
|
+
|
|
821
|
+
const swapIxns = await activeSwapper.getSwapIxns(swapQuote);
|
|
822
|
+
|
|
823
|
+
const collateralTokenProgram = await getTokenProgramForMintFromRpc(
|
|
824
|
+
this.connection,
|
|
825
|
+
collateralMint,
|
|
826
|
+
);
|
|
827
|
+
const debtTokenProgram = await getTokenProgramForMintFromRpc(
|
|
828
|
+
this.connection,
|
|
829
|
+
debtMint,
|
|
830
|
+
);
|
|
831
|
+
|
|
832
|
+
const withdrawIx = await this.program.clendAccountWithdraw(
|
|
833
|
+
vault,
|
|
834
|
+
vaultManager,
|
|
835
|
+
clendGroup,
|
|
836
|
+
clendAccount,
|
|
837
|
+
collateralMint,
|
|
838
|
+
collateralTokenProgram,
|
|
839
|
+
collateralToWithdraw,
|
|
840
|
+
remainingAccounts,
|
|
841
|
+
);
|
|
842
|
+
|
|
843
|
+
// if withdrawAll == true we need to make sure we withdraw emissions from both banks before withdrawing all the bank tokens
|
|
844
|
+
const emissionsIxns: web3.TransactionInstruction[] = [];
|
|
845
|
+
if (withdrawAll) {
|
|
846
|
+
// get bank data
|
|
847
|
+
const collateralBankData = activeBankData.find((b) =>
|
|
848
|
+
b.mint.equals(collateralMint),
|
|
849
|
+
);
|
|
850
|
+
if (!collateralBankData) {
|
|
851
|
+
throw new Error(
|
|
852
|
+
`Collateral bank not found: ${collateralMint.toString()}`,
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
const debtBankData = activeBankData.find((b) => b.mint.equals(debtMint));
|
|
856
|
+
if (!debtBankData) {
|
|
857
|
+
throw new Error(`Debt bank not found: ${debtMint.toString()}`);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
//// collateral bank emissions
|
|
861
|
+
//if (
|
|
862
|
+
// !collateralBankData.emissionsMint.equals(web3.PublicKey.default) &&
|
|
863
|
+
// (collateralBankData.flags === BankFlags.LendingEmissionsActive ||
|
|
864
|
+
// collateralBankData.flags ===
|
|
865
|
+
// BankFlags.LendingAndBorrowingEmissionsActive)
|
|
866
|
+
//) {
|
|
867
|
+
// const emissionsMint = collateralBankData.emissionsMint;
|
|
868
|
+
// const emissionsMintTokenProgram = await getTokenProgramForMintFromRpc(
|
|
869
|
+
// this.connection,
|
|
870
|
+
// emissionsMint,
|
|
871
|
+
// );
|
|
872
|
+
// const withdrawEmissionsIx = await this.program.clendAccountClaimEmissions(
|
|
873
|
+
|
|
874
|
+
// clendGroup,
|
|
875
|
+
// clendAccount,
|
|
876
|
+
// clendAccountData.authority,
|
|
877
|
+
// collateralBank,
|
|
878
|
+
// emissionsMint,
|
|
879
|
+
// emissionsMintTokenProgram,
|
|
880
|
+
// );
|
|
881
|
+
// emissionsIxns.push(...withdrawEmissionsIx);
|
|
882
|
+
//}
|
|
883
|
+
|
|
884
|
+
//// debt bank emissions
|
|
885
|
+
//if (
|
|
886
|
+
// !debtBankData.emissionsMint.equals(web3.PublicKey.default) &&
|
|
887
|
+
// (debtBankData.flags === BankFlags.BorrowEmissionsActive ||
|
|
888
|
+
// debtBankData.flags === BankFlags.LendingAndBorrowingEmissionsActive)
|
|
889
|
+
//) {
|
|
890
|
+
// const emissionsMint = debtBankData.emissionsMint;
|
|
891
|
+
// const emissionsMintTokenProgram = await getTokenProgramForMintFromRpc(
|
|
892
|
+
// this.connection,
|
|
893
|
+
// emissionsMint,
|
|
894
|
+
// );
|
|
895
|
+
// const withdrawEmissionsIx = await this.program.clendAccountWithdrawEmissions(
|
|
896
|
+
// clendGroup,
|
|
897
|
+
// clendAccount,
|
|
898
|
+
// clendAccountData.authority,
|
|
899
|
+
// debtBank,
|
|
900
|
+
// emissionsMint,
|
|
901
|
+
// emissionsMintTokenProgram,
|
|
902
|
+
// );
|
|
903
|
+
// emissionsIxns.push(...withdrawEmissionsIx);
|
|
904
|
+
//}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Create repay instruction with the amount from the 'ExactIn' swap
|
|
908
|
+
const repayIx = await this.program.clendAccountRepay(
|
|
909
|
+
vault,
|
|
910
|
+
vaultManager,
|
|
911
|
+
clendGroup,
|
|
912
|
+
clendAccount,
|
|
913
|
+
debtMint,
|
|
914
|
+
debtTokenProgram,
|
|
915
|
+
finalDebtToRepay,
|
|
916
|
+
remainingAccounts,
|
|
917
|
+
);
|
|
918
|
+
|
|
919
|
+
const ixnsWithoutFlashLoan: web3.TransactionInstruction[] = [
|
|
920
|
+
...emissionsIxns,
|
|
921
|
+
...withdrawIx,
|
|
922
|
+
...swapIxns.ixns,
|
|
923
|
+
...repayIx,
|
|
924
|
+
];
|
|
925
|
+
|
|
926
|
+
const cuIxns = 2;
|
|
927
|
+
const endIndex = new BN(
|
|
928
|
+
cuIxns + ixnsWithoutFlashLoan.length + 1 + additionalIxnCount,
|
|
929
|
+
);
|
|
930
|
+
const remainingAccountsForFlashLoan =
|
|
931
|
+
getClendAccountRemainingAccounts(activeBankData);
|
|
932
|
+
|
|
933
|
+
const { beginFlashLoanIx, endFlashLoanIx } =
|
|
934
|
+
await this.clendClient.instructions.createFlashLoanInstructions(
|
|
935
|
+
clendAccount,
|
|
936
|
+
user,
|
|
937
|
+
endIndex,
|
|
938
|
+
remainingAccountsForFlashLoan,
|
|
939
|
+
);
|
|
940
|
+
|
|
941
|
+
const instructions = [
|
|
942
|
+
beginFlashLoanIx,
|
|
943
|
+
...ixnsWithoutFlashLoan,
|
|
944
|
+
endFlashLoanIx,
|
|
945
|
+
];
|
|
946
|
+
|
|
947
|
+
return { ixns: instructions, luts: swapIxns.luts };
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
async getWithdrawLeverageDebtIxns(
|
|
951
|
+
vault: web3.PublicKey,
|
|
952
|
+
vaultManager: web3.PublicKey,
|
|
953
|
+
user: web3.PublicKey,
|
|
954
|
+
clendGroup: web3.PublicKey,
|
|
955
|
+
clendAccount: web3.PublicKey,
|
|
956
|
+
collateralMint: web3.PublicKey,
|
|
957
|
+
debtMint: web3.PublicKey,
|
|
958
|
+
collateralDecimals: number,
|
|
959
|
+
debtDecimals: number,
|
|
960
|
+
withdrawAll: boolean,
|
|
961
|
+
debtToRepay: BN,
|
|
962
|
+
collateralToWithdraw: BN,
|
|
963
|
+
slippageBps: number,
|
|
964
|
+
additionalIxnCount: number,
|
|
965
|
+
swapperOverride?: ISwapper,
|
|
966
|
+
): Promise<{ ixns: web3.TransactionInstruction[]; luts: web3.PublicKey[] }> {
|
|
967
|
+
// override swapper if provided
|
|
968
|
+
let activeSwapper = this.swapper;
|
|
969
|
+
if (swapperOverride) {
|
|
970
|
+
activeSwapper = swapperOverride;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
const clendAccountData =
|
|
974
|
+
await this.clendClient.getClendAccount(clendAccount);
|
|
975
|
+
if (!clendAccountData) {
|
|
976
|
+
throw new Error(`Clend account not found: ${clendAccount.toString()}`);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
let activeBanks = getClendAccountActiveBanks(clendAccountData);
|
|
980
|
+
const activeBankData = await this.clendClient.getBanks(activeBanks);
|
|
981
|
+
const remainingAccounts = getClendAccountRemainingAccounts(activeBankData);
|
|
982
|
+
|
|
983
|
+
// Get a fresh 'ExactIn' quote for swapping the total collateral to withdraw
|
|
984
|
+
const swapQuote = await activeSwapper.getQuote({
|
|
985
|
+
payer: vault,
|
|
986
|
+
inputMint: collateralMint,
|
|
987
|
+
inputMintDecimals: collateralDecimals,
|
|
988
|
+
outputMint: debtMint,
|
|
989
|
+
outputMintDecimals: debtDecimals,
|
|
990
|
+
inputAmount: collateralToWithdraw,
|
|
991
|
+
slippageBps,
|
|
992
|
+
swapMode: "ExactIn",
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
// The minimum amount of debt token we are guaranteed to receive from the swap
|
|
996
|
+
const minDebtReceivedFromSwap = swapQuote.outAmount;
|
|
997
|
+
|
|
998
|
+
// Determine the final amount to repay. It's the lesser of what we need and what we're guaranteed to get.
|
|
999
|
+
const finalDebtToRepay = BN.min(debtToRepay, minDebtReceivedFromSwap);
|
|
1000
|
+
|
|
1001
|
+
// Get Swap Instructions from the swapper
|
|
1002
|
+
const swapIxns = await activeSwapper.getSwapIxns(swapQuote);
|
|
1003
|
+
|
|
1004
|
+
// --- ATAs and Clend Instructions ---
|
|
1005
|
+
const collateralTokenProgram = getTokenProgramForMint(collateralMint);
|
|
1006
|
+
const debtTokenProgram = getTokenProgramForMint(debtMint);
|
|
1007
|
+
// Create withdraw instruction: Withdraw the total collateral calculated by the params function
|
|
1008
|
+
const withdrawIx = await this.program.clendAccountWithdraw(
|
|
1009
|
+
vault,
|
|
1010
|
+
vaultManager,
|
|
1011
|
+
clendGroup,
|
|
1012
|
+
clendAccount,
|
|
1013
|
+
collateralMint,
|
|
1014
|
+
collateralTokenProgram,
|
|
1015
|
+
collateralToWithdraw,
|
|
1016
|
+
remainingAccounts,
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
// if withdrawAll == true we need to make sure we withdraw emissions from both banks before withdrawing all the bank tokens
|
|
1020
|
+
const emissionsIxns: web3.TransactionInstruction[] = [];
|
|
1021
|
+
if (withdrawAll) {
|
|
1022
|
+
// get bank data
|
|
1023
|
+
const collateralBankData = activeBankData.find((b) =>
|
|
1024
|
+
b.mint.equals(collateralMint),
|
|
1025
|
+
);
|
|
1026
|
+
if (!collateralBankData) {
|
|
1027
|
+
throw new Error(
|
|
1028
|
+
`Collateral bank not found: ${collateralMint.toString()}`,
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
const debtBankData = activeBankData.find((b) => b.mint.equals(debtMint));
|
|
1032
|
+
if (!debtBankData) {
|
|
1033
|
+
throw new Error(`Debt bank not found: ${debtMint.toString()}`);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
//// collateral bank emissions
|
|
1037
|
+
//if (
|
|
1038
|
+
// !collateralBankData.emissionsMint.equals(web3.PublicKey.default) &&
|
|
1039
|
+
// (collateralBankData.flags === BankFlags.LendingEmissionsActive ||
|
|
1040
|
+
// collateralBankData.flags ===
|
|
1041
|
+
// BankFlags.LendingAndBorrowingEmissionsActive)
|
|
1042
|
+
//) {
|
|
1043
|
+
// const emissionsMint = collateralBankData.emissionsMint;
|
|
1044
|
+
// const emissionsMintTokenProgram = await getTokenProgramForMintFromRpc(
|
|
1045
|
+
// this.connection,
|
|
1046
|
+
// emissionsMint,
|
|
1047
|
+
// );
|
|
1048
|
+
// const withdrawEmissionsIx = await this.instructions.withdrawEmissions(
|
|
1049
|
+
// clendGroup,
|
|
1050
|
+
// clendAccount,
|
|
1051
|
+
// clendAccountData.authority,
|
|
1052
|
+
// collateralBank,
|
|
1053
|
+
// emissionsMint,
|
|
1054
|
+
// emissionsMintTokenProgram,
|
|
1055
|
+
// );
|
|
1056
|
+
// emissionsIxns.push(...withdrawEmissionsIx);
|
|
1057
|
+
//}
|
|
1058
|
+
|
|
1059
|
+
//// debt bank emissions
|
|
1060
|
+
//if (
|
|
1061
|
+
// !debtBankData.emissionsMint.equals(web3.PublicKey.default) &&
|
|
1062
|
+
// (debtBankData.flags === BankFlags.BorrowEmissionsActive ||
|
|
1063
|
+
// debtBankData.flags === BankFlags.LendingAndBorrowingEmissionsActive)
|
|
1064
|
+
//) {
|
|
1065
|
+
// const emissionsMint = debtBankData.emissionsMint;
|
|
1066
|
+
// const emissionsMintTokenProgram = await getTokenProgramForMintFromRpc(
|
|
1067
|
+
// this.connection,
|
|
1068
|
+
// emissionsMint,
|
|
1069
|
+
// );
|
|
1070
|
+
// const withdrawEmissionsIx = await this.instructions.withdrawEmissions(
|
|
1071
|
+
// clendGroup,
|
|
1072
|
+
// clendAccount,
|
|
1073
|
+
// clendAccountData.authority,
|
|
1074
|
+
// debtBank,
|
|
1075
|
+
// emissionsMint,
|
|
1076
|
+
// emissionsMintTokenProgram,
|
|
1077
|
+
// );
|
|
1078
|
+
// emissionsIxns.push(...withdrawEmissionsIx);
|
|
1079
|
+
//}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// Create repay instruction: Repay the debt portion using the guaranteed swap output
|
|
1083
|
+
const repayIx = await this.program.clendAccountRepay(
|
|
1084
|
+
vault,
|
|
1085
|
+
vaultManager,
|
|
1086
|
+
clendGroup,
|
|
1087
|
+
clendAccount,
|
|
1088
|
+
debtMint,
|
|
1089
|
+
debtTokenProgram,
|
|
1090
|
+
finalDebtToRepay, // Use the slippage-protected amount from the final quote
|
|
1091
|
+
remainingAccounts,
|
|
1092
|
+
);
|
|
1093
|
+
|
|
1094
|
+
// Assemble instructions
|
|
1095
|
+
const ixnsWithoutFlashLoan: web3.TransactionInstruction[] = [
|
|
1096
|
+
...emissionsIxns,
|
|
1097
|
+
...withdrawIx,
|
|
1098
|
+
...swapIxns.ixns,
|
|
1099
|
+
...repayIx,
|
|
1100
|
+
];
|
|
1101
|
+
|
|
1102
|
+
// Flash Loan Wrapping
|
|
1103
|
+
const cuIxns = 2;
|
|
1104
|
+
const endIndex = new BN(
|
|
1105
|
+
cuIxns + ixnsWithoutFlashLoan.length + 1 + additionalIxnCount,
|
|
1106
|
+
);
|
|
1107
|
+
const remainingAccountsForFlashLoan =
|
|
1108
|
+
getClendAccountRemainingAccounts(activeBankData);
|
|
1109
|
+
|
|
1110
|
+
const { beginFlashLoanIx, endFlashLoanIx } =
|
|
1111
|
+
await this.clendClient.instructions.createFlashLoanInstructions(
|
|
1112
|
+
clendAccount,
|
|
1113
|
+
user,
|
|
1114
|
+
endIndex,
|
|
1115
|
+
remainingAccountsForFlashLoan,
|
|
1116
|
+
);
|
|
1117
|
+
|
|
1118
|
+
const instructions = [
|
|
1119
|
+
beginFlashLoanIx,
|
|
1120
|
+
...ixnsWithoutFlashLoan,
|
|
1121
|
+
endFlashLoanIx,
|
|
1122
|
+
];
|
|
1123
|
+
|
|
1124
|
+
return { ixns: instructions, luts: swapIxns.luts };
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
async swap(
|
|
1128
|
+
vault: web3.PublicKey,
|
|
1129
|
+
inputMint: web3.PublicKey,
|
|
1130
|
+
outputMint: web3.PublicKey,
|
|
1131
|
+
inAmount: BN,
|
|
1132
|
+
): Promise<string> {
|
|
1133
|
+
const vaultAssetInReserve = getAssociatedTokenAddressSync(
|
|
1134
|
+
inputMint,
|
|
1135
|
+
vault,
|
|
1136
|
+
true,
|
|
1137
|
+
TOKEN_PROGRAM_ID,
|
|
1138
|
+
);
|
|
1139
|
+
const vaultAssetOutReserve = getAssociatedTokenAddressSync(
|
|
1140
|
+
outputMint,
|
|
1141
|
+
vault,
|
|
1142
|
+
true,
|
|
1143
|
+
TOKEN_PROGRAM_ID,
|
|
1144
|
+
);
|
|
1145
|
+
|
|
1146
|
+
const quote = await this.swapper.getQuote({
|
|
1147
|
+
payer: vault,
|
|
1148
|
+
inputMint: inputMint,
|
|
1149
|
+
inputMintDecimals: 6,
|
|
1150
|
+
outputMint: outputMint,
|
|
1151
|
+
outputMintDecimals: 6,
|
|
1152
|
+
inputAmount: inAmount,
|
|
1153
|
+
swapMode: "ExactIn",
|
|
1154
|
+
slippageBps: 100,
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
// TODO: no setup ixns
|
|
1158
|
+
const swapIxns = await this.swapper.getSwapIxns(quote);
|
|
1159
|
+
const swapIx = swapIxns.ixns.find((ix) =>
|
|
1160
|
+
ix.programId.equals(JUPITER_SWAP_PROGRAM_ID),
|
|
1161
|
+
)!;
|
|
1162
|
+
const swapRemainingAccounts: web3.AccountMeta[] = swapIx.keys.map(
|
|
1163
|
+
(key) => ({
|
|
1164
|
+
pubkey: key.pubkey,
|
|
1165
|
+
// If the account is the vault PDA, mark it as NOT a signer for the client transaction.
|
|
1166
|
+
isSigner: key.pubkey.equals(vault) ? false : key.isSigner,
|
|
1167
|
+
isWritable: key.isWritable,
|
|
1168
|
+
}),
|
|
1169
|
+
);
|
|
1170
|
+
const swapData = swapIx.data;
|
|
1171
|
+
|
|
1172
|
+
const ixns = await this.program.swap(
|
|
1173
|
+
vault,
|
|
1174
|
+
this.address(),
|
|
1175
|
+
inputMint,
|
|
1176
|
+
outputMint,
|
|
1177
|
+
vaultAssetInReserve,
|
|
1178
|
+
vaultAssetOutReserve,
|
|
1179
|
+
swapData,
|
|
1180
|
+
swapRemainingAccounts,
|
|
1181
|
+
JUPITER_SWAP_PROGRAM_ID,
|
|
1182
|
+
);
|
|
1183
|
+
const txSig = await this.send(ixns);
|
|
1184
|
+
|
|
1185
|
+
return txSig;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
async distributeFees(
|
|
1189
|
+
vault: web3.PublicKey,
|
|
1190
|
+
sharesDestinationOwner: web3.PublicKey,
|
|
1191
|
+
): Promise<string> {
|
|
1192
|
+
// fetch vault data
|
|
1193
|
+
const vaultData = await this.getVault(vault);
|
|
1194
|
+
const manager = vaultData.manager;
|
|
1195
|
+
const shares = vaultData.sharesMint;
|
|
1196
|
+
|
|
1197
|
+
const ixns = await this.program.distributeFees(
|
|
1198
|
+
vault,
|
|
1199
|
+
manager,
|
|
1200
|
+
shares,
|
|
1201
|
+
sharesDestinationOwner,
|
|
1202
|
+
);
|
|
1203
|
+
|
|
1204
|
+
const txSig = await this.send(ixns);
|
|
1205
|
+
|
|
1206
|
+
return txSig;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
async send(
|
|
1210
|
+
ixns: web3.TransactionInstruction[],
|
|
1211
|
+
additionalSigners: web3.Signer[] = [],
|
|
1212
|
+
): Promise<string> {
|
|
1213
|
+
const { blockhash } = await this.connection.getLatestBlockhash("confirmed");
|
|
1214
|
+
const msg = new web3.TransactionMessage({
|
|
1215
|
+
payerKey: this.address(),
|
|
1216
|
+
recentBlockhash: blockhash,
|
|
1217
|
+
instructions: ixns,
|
|
1218
|
+
}).compileToV0Message();
|
|
1219
|
+
|
|
1220
|
+
const tx = new web3.VersionedTransaction(msg);
|
|
1221
|
+
|
|
1222
|
+
const signedTx =
|
|
1223
|
+
await this.program.program.provider.wallet!.signTransaction(tx);
|
|
1224
|
+
signedTx.sign(additionalSigners);
|
|
1225
|
+
|
|
1226
|
+
try {
|
|
1227
|
+
const txSig = await this.connection.sendRawTransaction(
|
|
1228
|
+
signedTx.serialize(),
|
|
1229
|
+
{
|
|
1230
|
+
skipPreflight: this.skipPreflight,
|
|
1231
|
+
},
|
|
1232
|
+
);
|
|
1233
|
+
|
|
1234
|
+
// will throw an error if not found or tx errored
|
|
1235
|
+
await this.confirmTx(txSig);
|
|
1236
|
+
|
|
1237
|
+
return txSig;
|
|
1238
|
+
} catch (e) {
|
|
1239
|
+
if (e instanceof web3.SendTransactionError) {
|
|
1240
|
+
throw new Error(`tx failed: ${e}`);
|
|
1241
|
+
}
|
|
1242
|
+
throw e;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
async confirmTx(txSig: string): Promise<void> {
|
|
1247
|
+
const maxAttempts = 90;
|
|
1248
|
+
const sleepDurationMs = 1000; // 1 sec
|
|
1249
|
+
|
|
1250
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1251
|
+
const txResult = await this.connection.getSignatureStatuses([txSig], {
|
|
1252
|
+
searchTransactionHistory: false,
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
if (txResult.value.length > 0 && txResult.value[0] !== null) {
|
|
1256
|
+
const txSigResult = txResult.value[0];
|
|
1257
|
+
if (txSigResult.err !== null) {
|
|
1258
|
+
throw new Error(
|
|
1259
|
+
`tx returned an error: ${txSigResult.err.toString()}`,
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
return; // Transaction confirmed successfully
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
if (attempt < maxAttempts - 1) {
|
|
1266
|
+
// sleep before next attempt
|
|
1267
|
+
await new Promise((resolve) => setTimeout(resolve, sleepDurationMs));
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// If we've exhausted all attempts, throw the error
|
|
1272
|
+
throw new Error(`tx sig ${txSig} not found after ${maxAttempts} attempts`);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
export async function buildEquityRemainingAccounts(
|
|
1277
|
+
vaultData: Vault,
|
|
1278
|
+
clendClient: ClendClient,
|
|
1279
|
+
): Promise<AccountMeta[]> {
|
|
1280
|
+
// gather all asset mints, oracles, reserves, and clend accounts
|
|
1281
|
+
const assetMints: web3.PublicKey[] = vaultData.assets.map(
|
|
1282
|
+
(a) => a.mint.address,
|
|
1283
|
+
);
|
|
1284
|
+
const assetOracles: web3.PublicKey[] = vaultData.assets.map((a) => a.oracle);
|
|
1285
|
+
const assetReserves: web3.PublicKey[] = vaultData.assets.map(
|
|
1286
|
+
(a) => a.reserve.address,
|
|
1287
|
+
);
|
|
1288
|
+
const clendAccounts: web3.PublicKey[] = vaultData.clendAccounts.map(
|
|
1289
|
+
(ca) => ca.address,
|
|
1290
|
+
);
|
|
1291
|
+
|
|
1292
|
+
const base: AccountMeta[] = [
|
|
1293
|
+
// mints
|
|
1294
|
+
...assetMints.map((a) => ({
|
|
1295
|
+
pubkey: a,
|
|
1296
|
+
isSigner: false,
|
|
1297
|
+
isWritable: false,
|
|
1298
|
+
})),
|
|
1299
|
+
// oracles
|
|
1300
|
+
...assetOracles.map((a) => ({
|
|
1301
|
+
pubkey: a,
|
|
1302
|
+
isSigner: false,
|
|
1303
|
+
isWritable: false,
|
|
1304
|
+
})),
|
|
1305
|
+
// reserves
|
|
1306
|
+
...assetReserves.map((a) => ({
|
|
1307
|
+
pubkey: a,
|
|
1308
|
+
isSigner: false,
|
|
1309
|
+
isWritable: false,
|
|
1310
|
+
})),
|
|
1311
|
+
// clend account addrs
|
|
1312
|
+
...clendAccounts.map((ca) => ({
|
|
1313
|
+
pubkey: ca,
|
|
1314
|
+
isSigner: false,
|
|
1315
|
+
isWritable: false,
|
|
1316
|
+
})),
|
|
1317
|
+
];
|
|
1318
|
+
|
|
1319
|
+
// include each clend account's active banks + their oracles
|
|
1320
|
+
const bankAndOracleMetas: AccountMeta[] = [];
|
|
1321
|
+
for (const ca of clendAccounts) {
|
|
1322
|
+
const state = await clendClient.getClendAccount(ca);
|
|
1323
|
+
if (!state) {
|
|
1324
|
+
throw new Error(`Clend account not found: ${ca.toString()}`);
|
|
1325
|
+
}
|
|
1326
|
+
const activeBanks = getClendAccountActiveBanks(state);
|
|
1327
|
+
|
|
1328
|
+
const activeBankState: Bank[] = [];
|
|
1329
|
+
for (const bank of activeBanks) {
|
|
1330
|
+
const bankData = await clendClient.getBank(bank);
|
|
1331
|
+
activeBankState.push(bankData);
|
|
1332
|
+
}
|
|
1333
|
+
const rem = getClendAccountRemainingAccounts(activeBankState);
|
|
1334
|
+
bankAndOracleMetas.push(...rem);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// de-duplicate metas by pubkey, preserving writability where needed
|
|
1338
|
+
const byPk = new Map<string, AccountMeta>();
|
|
1339
|
+
const absorb = (m: AccountMeta) => {
|
|
1340
|
+
const k = m.pubkey.toBase58();
|
|
1341
|
+
const prev = byPk.get(k);
|
|
1342
|
+
if (!prev) byPk.set(k, m);
|
|
1343
|
+
else
|
|
1344
|
+
byPk.set(k, {
|
|
1345
|
+
pubkey: m.pubkey,
|
|
1346
|
+
isSigner: prev.isSigner || m.isSigner,
|
|
1347
|
+
isWritable: prev.isWritable || m.isWritable,
|
|
1348
|
+
});
|
|
1349
|
+
};
|
|
1350
|
+
[...base, ...bankAndOracleMetas].forEach(absorb);
|
|
1351
|
+
|
|
1352
|
+
const remAccounts = [...byPk.values()];
|
|
1353
|
+
return remAccounts;
|
|
1354
|
+
}
|