From 8fe7a0ad71e52915d1579fef60cd4cbfd5ff45f1 Mon Sep 17 00:00:00 2001 From: 0xodia <0xodia@solend.fi> Date: Mon, 29 Apr 2024 15:23:45 -0400 Subject: [PATCH] new sdk layout --- solend-sdk/src/core/actions.ts | 123 +++++++-- solend-sdk/src/core/margin.ts | 272 ++++++++++++++----- solend-sdk/src/instructions/index.ts | 1 + solend-sdk/src/instructions/withdrawExact.ts | 59 ++++ solend-sdk/src/state/reserve.ts | 12 +- 5 files changed, 373 insertions(+), 94 deletions(-) create mode 100644 solend-sdk/src/instructions/withdrawExact.ts diff --git a/solend-sdk/src/core/actions.ts b/solend-sdk/src/core/actions.ts index c8b7841b..b2ef480b 100644 --- a/solend-sdk/src/core/actions.ts +++ b/solend-sdk/src/core/actions.ts @@ -1,10 +1,13 @@ import { + AddressLookupTableAccount, Connection, PublicKey, SystemProgram, Transaction, TransactionInstruction, + TransactionMessage, TransactionSignature, + VersionedTransaction, } from "@solana/web3.js"; import { NATIVE_MINT, @@ -94,6 +97,8 @@ export class SolendActionCore { borrowReserves: Array; + lookupTableAccount?: AddressLookupTableAccount; + private constructor( programId: PublicKey, connection: Connection, @@ -109,7 +114,8 @@ export class SolendActionCore { amount: BN, depositReserves: Array, borrowReserves: Array, - hostAta?: PublicKey + hostAta?: PublicKey, + lookupTableAccount?: AddressLookupTableAccount ) { this.programId = programId; this.connection = connection; @@ -131,6 +137,7 @@ export class SolendActionCore { this.postTxnIxs = []; this.depositReserves = depositReserves; this.borrowReserves = borrowReserves; + this.lookupTableAccount = lookupTableAccount; } static async initialize( @@ -143,7 +150,8 @@ export class SolendActionCore { environment: EnvironmentType = "production", customObligationAddress?: PublicKey, hostAta?: PublicKey, - customObligationSeed?: string + customObligationSeed?: string, + lookupTableAddress?: PublicKey ) { const seed = customObligationSeed ?? pool.address.slice(0, 32); const programId = getProgramId(environment); @@ -211,6 +219,10 @@ export class SolendActionCore { true ); + const lookupTableAccount = lookupTableAddress + ? (await connection.getAddressLookupTable(lookupTableAddress)).value + : undefined; + return new SolendActionCore( programId, connection, @@ -226,7 +238,8 @@ export class SolendActionCore { amount, depositReserves, borrowReserves, - hostAta + hostAta, + lookupTableAccount ?? undefined ); } @@ -237,7 +250,8 @@ export class SolendActionCore { amount: string, publicKey: PublicKey, obligationAddress: PublicKey, - environment: EnvironmentType = "production" + environment: EnvironmentType = "production", + lookupTableAddress?: PublicKey ) { const axn = await SolendActionCore.initialize( pool, @@ -247,7 +261,10 @@ export class SolendActionCore { publicKey, connection, environment, - obligationAddress + obligationAddress, + undefined, + undefined, + lookupTableAddress ); await axn.addSupportIxs("forgive"); @@ -264,7 +281,8 @@ export class SolendActionCore { publicKey: PublicKey, environment: EnvironmentType = "production", obligationAddress?: PublicKey, - obligationSeed?: string + obligationSeed?: string, + lookupTableAddress?: PublicKey ) { const axn = await SolendActionCore.initialize( pool, @@ -276,7 +294,8 @@ export class SolendActionCore { environment, obligationAddress, undefined, - obligationSeed + obligationSeed, + lookupTableAddress ); await axn.addSupportIxs("deposit"); @@ -292,7 +311,8 @@ export class SolendActionCore { amount: string, publicKey: PublicKey, environment: EnvironmentType = "production", - hostAta?: PublicKey + hostAta?: PublicKey, + lookupTableAddress?: PublicKey ) { const axn = await SolendActionCore.initialize( pool, @@ -303,7 +323,9 @@ export class SolendActionCore { connection, environment, undefined, - hostAta + hostAta, + undefined, + lookupTableAddress ); await axn.addSupportIxs("borrow"); @@ -317,7 +339,8 @@ export class SolendActionCore { connection: Connection, amount: string, publicKey: PublicKey, - environment: EnvironmentType = "production" + environment: EnvironmentType = "production", + lookupTableAddress?: PublicKey ) { const axn = await SolendActionCore.initialize( pool, @@ -326,7 +349,11 @@ export class SolendActionCore { new BN(amount), publicKey, connection, - environment + environment, + undefined, + undefined, + undefined, + lookupTableAddress ); await axn.addSupportIxs("mint"); await axn.addDepositReserveLiquidityIx(); @@ -339,7 +366,8 @@ export class SolendActionCore { connection: Connection, amount: string, publicKey: PublicKey, - environment: EnvironmentType = "production" + environment: EnvironmentType = "production", + lookupTableAddress?: PublicKey ) { const axn = await SolendActionCore.initialize( pool, @@ -348,7 +376,11 @@ export class SolendActionCore { new BN(amount), publicKey, connection, - environment + environment, + undefined, + undefined, + undefined, + lookupTableAddress ); await axn.addSupportIxs("redeem"); await axn.addRedeemReserveCollateralIx(); @@ -361,7 +393,8 @@ export class SolendActionCore { connection: Connection, amount: string, publicKey: PublicKey, - environment: EnvironmentType = "production" + environment: EnvironmentType = "production", + lookupTableAddress?: PublicKey ) { const axn = await SolendActionCore.initialize( pool, @@ -370,7 +403,11 @@ export class SolendActionCore { new BN(amount), publicKey, connection, - environment + environment, + undefined, + undefined, + undefined, + lookupTableAddress ); await axn.addSupportIxs("depositCollateral"); await axn.addDepositObligationCollateralIx(); @@ -383,7 +420,8 @@ export class SolendActionCore { connection: Connection, amount: string, publicKey: PublicKey, - environment: EnvironmentType = "production" + environment: EnvironmentType = "production", + lookupTableAddress?: PublicKey ) { const axn = await SolendActionCore.initialize( pool, @@ -392,7 +430,11 @@ export class SolendActionCore { new BN(amount), publicKey, connection, - environment + environment, + undefined, + undefined, + undefined, + lookupTableAddress ); await axn.addSupportIxs("withdrawCollateral"); @@ -407,7 +449,10 @@ export class SolendActionCore { connection: Connection, amount: string, publicKey: PublicKey, - environment: EnvironmentType = "production" + environment: EnvironmentType = "production", + obligationAddress?: PublicKey, + obligationSeed?: string, + lookupTableAddress?: PublicKey ) { const axn = await SolendActionCore.initialize( pool, @@ -416,7 +461,11 @@ export class SolendActionCore { new BN(amount), publicKey, connection, - environment + environment, + obligationAddress, + undefined, + obligationSeed, + lookupTableAddress ); await axn.addSupportIxs("withdraw"); @@ -431,7 +480,8 @@ export class SolendActionCore { connection: Connection, amount: string, publicKey: PublicKey, - environment: EnvironmentType = "production" + environment: EnvironmentType = "production", + lookupTableAddress?: PublicKey ) { const axn = await SolendActionCore.initialize( pool, @@ -440,7 +490,11 @@ export class SolendActionCore { new BN(amount), publicKey, connection, - environment + environment, + undefined, + undefined, + undefined, + lookupTableAddress ); await axn.addSupportIxs("repay"); @@ -449,6 +503,24 @@ export class SolendActionCore { return axn; } + async getVersionedTransaction() { + return new VersionedTransaction( + new TransactionMessage({ + payerKey: this.publicKey, + recentBlockhash: (await this.connection.getRecentBlockhash()).blockhash, + instructions: [ + ...this.preTxnIxs, + ...this.setupIxs, + ...this.lendingIxs, + ...this.cleanupIxs, + ...this.postTxnIxs, + ], + }).compileToV0Message( + this.lookupTableAccount ? [this.lookupTableAccount] : [] + ) + ); + } + async getTransactions() { const txns: { preLendingTxn: Transaction | null; @@ -854,7 +926,7 @@ export class SolendActionCore { new PublicKey(this.reserve.mintAddress) ); - if (this.positions === POSITION_LIMIT && this.hostAta) { + if (!this.lookupTableAccount) { this.preTxnIxs.push(createUserTokenAccountIx); } else { this.setupIxs.push(createUserTokenAccountIx); @@ -876,10 +948,7 @@ export class SolendActionCore { new PublicKey(this.reserve.cTokenMint) ); - if ( - this.positions === POSITION_LIMIT && - this.reserve.mintAddress === NATIVE_MINT.toBase58() - ) { + if (!this.lookupTableAccount) { this.preTxnIxs.push(createUserCollateralAccountIx); } else { this.setupIxs.push(createUserCollateralAccountIx); @@ -985,7 +1054,7 @@ export class SolendActionCore { postIxs.push(closeWSOLAccountIx); } - if (this.positions && this.positions >= POSITION_LIMIT) { + if (!this.lookupTableAccount) { this.preTxnIxs.push(...preIxs); this.postTxnIxs.push(...postIxs); } else { diff --git a/solend-sdk/src/core/margin.ts b/solend-sdk/src/core/margin.ts index 75e0d63b..56004e72 100644 --- a/solend-sdk/src/core/margin.ts +++ b/solend-sdk/src/core/margin.ts @@ -9,8 +9,11 @@ import { } from "@solana/web3.js"; import { createAssociatedTokenAccountInstruction, + createCloseAccountInstruction, + createInitializeAccountInstruction, getAssociatedTokenAddress, getAssociatedTokenAddressSync, + TOKEN_PROGRAM_ID, } from "@solana/spl-token"; import { findProgramAddressSync } from "@project-serum/anchor/dist/cjs/utils/pubkey"; import { ObligationType, PoolType, ReserveType } from "."; @@ -28,12 +31,15 @@ import { refreshObligationInstruction, refreshReserveInstruction, repayObligationLiquidityInstruction, + withdrawExact, withdrawObligationCollateralAndRedeemReserveLiquidity, } from "../instructions"; import BigNumber from "bignumber.js"; import BN from "bn.js"; import JSBI from "jsbi"; import { SolendActionCore } from "./actions"; +import { repayMaxObligationLiquidityInstruction } from "../instructions/repayMaxObligationLiquidity"; +import { depositMaxReserveLiquidityAndObligationCollateralInstruction } from "../instructions/depositMaxReserveLiquidityAndObligationCollateral"; function dustAmountThreshold(decimals: number) { const dustDecimal = new BigNumber(decimals / 2).integerValue( @@ -67,7 +73,7 @@ export class Margin { shortReserveCollateralAta: PublicKey; - obligationSeed: string; + obligationSeed?: string; lendingMarketAuthority: PublicKey; @@ -82,7 +88,7 @@ export class Margin { shortReserve: ReserveType, pool: PoolType, obligationAddress: PublicKey, - obligationSeed: string, + obligationSeed?: string, obligation?: ObligationType, collateralReserve?: ReserveType ) { @@ -125,27 +131,27 @@ export class Margin { this.obligation && this.obligation.deposits.length > 0 ? this.obligation.deposits.map((ol) => new PublicKey(ol.reserveAddress)) : []; + this.borrowKeys = this.obligation && this.obligation.borrows.length > 0 ? this.obligation.borrows.map((ol) => new PublicKey(ol.reserveAddress)) : []; } - calculateMaxUserBorrowPower = () => { - console.log( - "minPriceBorrowLimit", - this.obligation?.minPriceBorrowLimit.toString() - ); - console.log(this.obligation); + calculateMaxUserBorrowPower = ( + closeMode?: "keepLong" | "keepShort", + exchangeRate?: string + ) => { + const shortTokenPrice = this.shortReserve.price; + const longTokenPrice = this.longReserve.price; + const conversion = exchangeRate + ? new BigNumber(exchangeRate) + : longTokenPrice.dividedBy(shortTokenPrice); + let curShortSupplyAmount = this.obligation?.deposits.find( (d) => d.reserveAddress === this.shortReserve.address )?.amount ?? new BigNumber(0); - console.log( - "short supply", - curShortSupplyAmount.toString(), - this.shortReserve.address - ); let curShortBorrowAmount = this.obligation?.borrows.find( (d) => d.reserveAddress === this.shortReserve.address @@ -158,6 +164,50 @@ export class Margin { this.obligation?.borrows.find( (d) => d.reserveAddress === this.longReserve.address )?.amount ?? new BigNumber(0); + + if (closeMode) { + const shortTokenToRepayAll = + closeMode === "keepLong" + ? conversion.multipliedBy(curLongBorrowAmount) + : curShortSupplyAmount; + + // convert to base and round up to match calculation logic in margin.tsx + const flashLoanFeeBase = shortTokenToRepayAll + .multipliedBy(this.shortReserve.flashLoanFee) + .multipliedBy(new BigNumber(10 ** this.shortReserve.decimals)) + .integerValue(BigNumber.ROUND_CEIL); + const flashLoanFee = new BigNumber( + flashLoanFeeBase + .dividedBy(new BigNumber(10 ** this.shortReserve.decimals)) + .toString() + ); + + let maxPossibleSwap = + closeMode === "keepLong" + ? shortTokenToRepayAll.plus(flashLoanFee) + : shortTokenToRepayAll.minus(flashLoanFee); + + // If user doesn't have enough short token supplied, we re-calculate given their max short token supplied + if ( + curShortSupplyAmount.isLessThan(maxPossibleSwap) && + closeMode === "keepLong" + ) { + // convert to base and round up to match calculation logic in margin.tsx + const flashLoanFeeBase = curShortSupplyAmount + .multipliedBy(this.shortReserve.flashLoanFee) + .multipliedBy(new BigNumber(10 ** this.shortReserve.decimals)) + .integerValue(BigNumber.ROUND_CEIL); + const flashLoanFee = new BigNumber( + flashLoanFeeBase + .dividedBy(new BigNumber(10 ** this.shortReserve.decimals)) + .toString() + ); + maxPossibleSwap = curShortSupplyAmount.minus(flashLoanFee); + } + + return maxPossibleSwap.toNumber(); + } + // handle the case where short token has non-zero ltv / infitie borrow weight e.g mSOL/stSOL if ( this.shortReserve.addedBorrowWeightBPS.toString() === U64_MAX || @@ -180,9 +230,6 @@ export class Margin { return maxPossibleSwap.toNumber(); } - const shortTokenPrice = this.shortReserve.price; - const longTokenPrice = this.longReserve.price; - const shortTokenBorrowWeight = new BigNumber( this.shortReserve.addedBorrowWeightBPS.toString() ) @@ -244,16 +291,14 @@ export class Margin { } totalShortSwapable = totalShortSwapable.plus(swapable); - const longRecievedInSwap = swapable - .times(shortTokenPrice) - .dividedBy(longTokenPrice); + const longRecievedInSwap = swapable.dividedBy(conversion); if (curLongBorrowAmount > longRecievedInSwap) { curLongBorrowAmount = curLongBorrowAmount.minus(longRecievedInSwap); curMaxPriceUserTotalWeightedBorrow = curMaxPriceUserTotalWeightedBorrow.minus( longRecievedInSwap - .times(longTokenPrice) + .times(this.longReserve.maxPrice) .times(longTokenBorrowWeight) ); } else { @@ -263,13 +308,13 @@ export class Margin { curMaxPriceUserTotalWeightedBorrow = curMaxPriceUserTotalWeightedBorrow.minus( curLongBorrowAmount - .times(longTokenPrice) + .times(this.longReserve.maxPrice) .times(longTokenBorrowWeight) ); curMinPriceBorrowLimit = curMinPriceBorrowLimit.plus( longRecievedInSwap .minus(curLongBorrowAmount) - .times(longTokenPrice) + .times(this.longReserve.minPrice) .times(new BigNumber(this.longReserve.loanToValueRatio)) ); curLongBorrowAmount = new BigNumber(0); @@ -279,16 +324,21 @@ export class Margin { return totalShortSwapable.times(new BigNumber(".975")).toNumber(); }; - setupTx = async (depositCollateralConfig?: { - collateralReserve: ReserveType; - amount: string; - }) => { + setupTx = async ( + lookupTableAccount?: AddressLookupTableAccount, + depositCollateralConfig?: { + collateralReserve: ReserveType; + amount: string; + } + ) => { const ixs: TransactionInstruction[] = []; // If we are depositing, the deposit instruction will handle creating the obligation if ( (!this.obligation || this.obligation.address === "empty") && !depositCollateralConfig ) { + if (!this.obligationSeed) + throw Error("Seed required for new obligations"); ixs.push( SystemProgram.createAccountWithSeed({ fromPubkey: this.owner, @@ -311,6 +361,7 @@ export class Margin { ) ); } + let longATA = PublicKey.default; await Promise.all( [ @@ -325,6 +376,18 @@ export class Margin { true ); + // track long token's ATA for cleanup later + if (mint.toString() === this.longReserve.mintAddress) { + longATA = tokenAccount; + } + + // if depositing collateral, ctoken ATA will be created by deposit txn + if ( + mint.toString() === this.longReserve.cTokenMint && + depositCollateralConfig + ) + return; + if (!(await this.connection.getAccountInfo(tokenAccount))) { ixs.push( createAssociatedTokenAccountInstruction( @@ -369,6 +432,41 @@ export class Margin { } } + // create a temp account to hold post swap tokens. gets closed in the clean up tx + const seed = `margin-${this.longReserve.symbol}-${this.pool.address}`.slice( + 0, + 32 + ); + + const longTmpAccount = await PublicKey.createWithSeed( + this.owner, + seed, + TOKEN_PROGRAM_ID + ); + + if (!(await this.connection.getAccountInfo(longTmpAccount))) { + ixs.push( + SystemProgram.createAccountWithSeed({ + fromPubkey: this.owner, + newAccountPubkey: longTmpAccount, + basePubkey: this.owner, + seed, + lamports: await this.connection.getMinimumBalanceForRentExemption( + 165 + ), + space: 165, + programId: TOKEN_PROGRAM_ID, + }) + ); + ixs.push( + createInitializeAccountInstruction( + longTmpAccount, + new PublicKey(this.longReserve.mintAddress), + this.owner + ) + ); + } + const blockhash = await this.connection .getLatestBlockhash() .then((res) => res.blockhash); @@ -377,16 +475,42 @@ export class Margin { payerKey: this.owner, recentBlockhash: blockhash, instructions: ixs, - }).compileToV0Message(); + }).compileToV0Message(lookupTableAccount ? [lookupTableAccount] : []); const tx = new VersionedTransaction(messageV0); return { - tx, + tx: ixs.length ? tx : null, obligationAddress: this.obligationAddress, + longTokenAccounts: { + longTmpAccount, + longATA, + }, }; }; + buildCleanupTx = async (longTmpAccount: PublicKey, longATA: PublicKey) => { + const ixs: TransactionInstruction[] = []; + + ixs.push( + createCloseAccountInstruction(longTmpAccount, longATA, this.owner, []) + ); + + const blockhash = await this.connection + .getLatestBlockhash() + .then((res) => res.blockhash); + + const messageV0 = new TransactionMessage({ + payerKey: this.owner, + recentBlockhash: blockhash, + instructions: ixs, + }).compileToV0Message(); + + const cleanupTx = new VersionedTransaction(messageV0); + + return cleanupTx; + }; + getSolendAccountCount = () => { const depositKeys = this.obligation && this.obligation.deposits.length > 0 @@ -564,6 +688,7 @@ export class Margin { SOLEND_PRODUCTION_PROGRAM_ID ) ); + const txn = new TransactionMessage({ payerKey: this.owner, recentBlockhash: "", @@ -580,7 +705,8 @@ export class Margin { slippageBps: number; }, swapInstructions: Array, - lookupTableAccounts: AddressLookupTableAccount[] + lookupTableAccounts: AddressLookupTableAccount[], + longTmpAccount: PublicKey ) => { const swapBaseBigNumber = new BigNumber(swapBaseAmount.toString()); const fee = swapBaseBigNumber @@ -622,6 +748,7 @@ export class Margin { this.obligation?.borrows ?.find((b) => b.reserveAddress === this.longReserve.address) ?.amount?.shiftedBy(this.longReserve.decimals) + ?.integerValue(BigNumber.ROUND_FLOOR) .toString() ?? "0"; const maxLongRepayAmount = BigNumber.min( prevLongBorrowAmount, @@ -631,20 +758,15 @@ export class Margin { longBalancePostSlippage.minus(maxLongRepayAmount); if (!maxLongRepayAmount.isZero()) { // non-zero deposit amount post repay means we need to max repay - let repayAmount; - if (longBalancePostRepay.isZero()) { - repayAmount = new BN(maxLongRepayAmount.toString()); - } else { - repayAmount = new BN(U64_MAX); + if (!longBalancePostRepay.isZero()) { this.borrowKeys = this.borrowKeys.filter( (k) => k.toString() !== this.longReserve.address ); } ixs.push( - repayObligationLiquidityInstruction( - repayAmount, - new PublicKey(this.longReserveLiquidityAta), + repayMaxObligationLiquidityInstruction( + longTmpAccount, new PublicKey(this.longReserve.liquidityAddress), new PublicKey(this.longReserve.address), new PublicKey(this.obligationAddress), @@ -665,9 +787,8 @@ export class Margin { } ixs.push( - depositReserveLiquidityAndObligationCollateralInstruction( - new BN(longBalancePostRepay.toString()), - new PublicKey(this.longReserveLiquidityAta), + depositMaxReserveLiquidityAndObligationCollateralInstruction( + longTmpAccount, new PublicKey(this.longReserveCollateralAta), new PublicKey(this.longReserve.address), new PublicKey(this.longReserve.liquidityAddress), @@ -690,7 +811,8 @@ export class Margin { const prevShortSupplyAmount = this.obligation?.deposits ?.find((b) => b.reserveAddress === this.shortReserve.address) - ?.amount?.shiftedBy(this.shortReserve.decimals) ?? new BigNumber("0"); + ?.amount?.shiftedBy(this.shortReserve.decimals) + ?.integerValue(BigNumber.ROUND_FLOOR) ?? new BigNumber("0"); const shortWithdrawAmount = BigNumber.min( flashLoanAmountWithFee, prevShortSupplyAmount @@ -750,35 +872,50 @@ export class Margin { .integerValue(BigNumber.ROUND_FLOOR) .toString() ); + + ixs.push( + withdrawExact( + withdrawCtokens, + new PublicKey(this.shortReserve.cTokenLiquidityAddress), + new PublicKey(this.shortReserveCollateralAta), + new PublicKey(this.shortReserve.address), + new PublicKey(this.shortReserveLiquidityAta), + new PublicKey(this.shortReserve.cTokenMint), + new PublicKey(this.shortReserve.liquidityAddress), + new PublicKey(this.obligationAddress), + new PublicKey(this.pool.address), + this.lendingMarketAuthority, + new PublicKey(this.owner), + new PublicKey(this.owner), + SOLEND_PRODUCTION_PROGRAM_ID + ) + ); } else { withdrawCtokens = new BN(U64_MAX); this.depositKeys = this.depositKeys.filter( (k) => k.toString() !== this.shortReserve.address ); - } - ixs.push( - withdrawObligationCollateralAndRedeemReserveLiquidity( - withdrawCtokens, - new PublicKey(this.shortReserve.cTokenLiquidityAddress), - new PublicKey(this.shortReserveCollateralAta), - new PublicKey(this.shortReserve.address), - new PublicKey(this.obligationAddress), - new PublicKey(this.pool.address), - this.lendingMarketAuthority, - new PublicKey(this.shortReserveLiquidityAta), - new PublicKey(this.shortReserve.cTokenMint), - new PublicKey(this.shortReserve.liquidityAddress), - new PublicKey(this.owner), - new PublicKey(this.owner), - SOLEND_PRODUCTION_PROGRAM_ID - ) - ); + ixs.push( + withdrawObligationCollateralAndRedeemReserveLiquidity( + withdrawCtokens, + new PublicKey(this.shortReserve.cTokenLiquidityAddress), + new PublicKey(this.shortReserveCollateralAta), + new PublicKey(this.shortReserve.address), + new PublicKey(this.obligationAddress), + new PublicKey(this.pool.address), + this.lendingMarketAuthority, + new PublicKey(this.shortReserveLiquidityAta), + new PublicKey(this.shortReserve.cTokenMint), + new PublicKey(this.shortReserve.liquidityAddress), + new PublicKey(this.owner), + new PublicKey(this.owner), + SOLEND_PRODUCTION_PROGRAM_ID + ) + ); + } } - console.log( - "borrow short token amount needed after withdrawwal: ", - shortBorrowAmountPostWithdrawal.toString() - ); + // 6) borrow short token amount to repay flash loan if necessary if (!shortBorrowAmountPostWithdrawal.isZero()) { const allKeys = this.depositKeys.concat(this.borrowKeys); @@ -858,6 +995,15 @@ export class Margin { .getLatestBlockhash() .then((res) => res.blockhash); + ixs.push( + createCloseAccountInstruction( + longTmpAccount, + this.longReserveCollateralAta, + this.owner, + [] + ) + ); + const messageV0 = new TransactionMessage({ payerKey: this.owner, recentBlockhash: blockhash, diff --git a/solend-sdk/src/instructions/index.ts b/solend-sdk/src/instructions/index.ts index a61dfe88..2c58c82d 100644 --- a/solend-sdk/src/instructions/index.ts +++ b/solend-sdk/src/instructions/index.ts @@ -18,4 +18,5 @@ export * from "./flashRepayReserveLiquidity"; export * from "./forgiveDebt"; export * from "./setLendingMarketOwnerAndConfig"; export * from "./updateMetadata"; +export * from "./withdrawExact"; export * from "./instruction"; diff --git a/solend-sdk/src/instructions/withdrawExact.ts b/solend-sdk/src/instructions/withdrawExact.ts new file mode 100644 index 00000000..1eb37a7d --- /dev/null +++ b/solend-sdk/src/instructions/withdrawExact.ts @@ -0,0 +1,59 @@ +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import BN from "bn.js"; +import { WRAPPER_PROGRAM_ID } from "./repayMaxObligationLiquidity"; +import * as Layout from "../layout"; + +const BufferLayout = require("buffer-layout"); + +/// Deposit liquidity into a reserve in exchange for collateral, and deposit the collateral as well. +export const withdrawExact = ( + liquidityAmount: number | BN, + reserveCollateral: PublicKey, + userCollateral: PublicKey, + withdrawReserve: PublicKey, + userLiquidity: PublicKey, + reserveCollateralMint: PublicKey, + reserveLiquiditySupply: PublicKey, + obligation: PublicKey, + lendingMarket: PublicKey, + lendingMarketAuthority: PublicKey, + obligationOwner: PublicKey, + transferAuthority: PublicKey, + solendProgramAddress: PublicKey +): TransactionInstruction => { + const dataLayout = BufferLayout.struct([ + BufferLayout.u8("instruction"), + Layout.uint64("liquidityAmount"), + ]); + + const data = Buffer.alloc(dataLayout.span); + dataLayout.encode( + { + instruction: 3, + liquidityAmount: new BN(liquidityAmount), + }, + data + ); + + const keys = [ + { pubkey: solendProgramAddress, isSigner: false, isWritable: false }, + { pubkey: reserveCollateral, isSigner: false, isWritable: true }, + { pubkey: userCollateral, isSigner: false, isWritable: true }, + { pubkey: withdrawReserve, isSigner: false, isWritable: true }, + { pubkey: obligation, isSigner: false, isWritable: true }, + { pubkey: lendingMarket, isSigner: false, isWritable: true }, + { pubkey: lendingMarketAuthority, isSigner: false, isWritable: false }, + { pubkey: userLiquidity, isSigner: false, isWritable: true }, + { pubkey: reserveCollateralMint, isSigner: false, isWritable: true }, + { pubkey: reserveLiquiditySupply, isSigner: false, isWritable: true }, + { pubkey: obligationOwner, isSigner: true, isWritable: false }, + { pubkey: transferAuthority, isSigner: true, isWritable: false }, + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + ]; + return new TransactionInstruction({ + keys, + programId: WRAPPER_PROGRAM_ID, + data, + }); +}; diff --git a/solend-sdk/src/state/reserve.ts b/solend-sdk/src/state/reserve.ts index 13befda7..34defda2 100644 --- a/solend-sdk/src/state/reserve.ts +++ b/solend-sdk/src/state/reserve.ts @@ -6,6 +6,7 @@ import * as fzstd from "fzstd"; import { RateLimiterLayout, RateLimiter } from "./rateLimiter"; import * as Layout from "../layout"; import { LastUpdate, LastUpdateLayout } from "./lastUpdate"; +import { U64_MAX } from "../core/constants"; const BufferLayout = require("buffer-layout"); @@ -217,10 +218,13 @@ function decodeReserve(buffer: Buffer): Reserve { protocolLiquidationFee: reserve.protocolLiquidationFee, protocolTakeRate: reserve.protocolTakeRate, addedBorrowWeightBPS: reserve.addedBorrowWeightBPS, - borrowWeight: new BigNumber(reserve.addedBorrowWeightBPS.toString()) - .dividedBy(new BigNumber(10000)) - .plus(new BigNumber(1)) - .toNumber(), + borrowWeight: + reserve.addedBorrowWeightBPS.toString() === U64_MAX + ? Number(U64_MAX) + : new BigNumber(reserve.addedBorrowWeightBPS.toString()) + .dividedBy(new BigNumber(10000)) + .plus(new BigNumber(1)) + .toNumber(), reserveType: reserve.reserveType == 0 ? AssetType.Regular : AssetType.Isolated, },