diff --git a/solend-sdk/src/core/actions.ts b/solend-sdk/src/core/actions.ts index fd796a0c..82b4b822 100644 --- a/solend-sdk/src/core/actions.ts +++ b/solend-sdk/src/core/actions.ts @@ -70,17 +70,17 @@ import { getSizeOfTransaction } from "../transaction"; const SOL_PADDING_FOR_INTEREST = "1000000"; type ActionConfigType = { - environment?: EnvironmentType, - customObligationAddress?: PublicKey, - hostAta?: PublicKey, - customObligationSeed?: string, - lookupTableAddress?: PublicKey, - tipAmount?: number, - repayReserve?: ReserveType, - token2022Mint?: string, - repayToken2022Mint?: string - debug?: boolean -} + environment?: EnvironmentType; + customObligationAddress?: PublicKey; + hostAta?: PublicKey; + customObligationSeed?: string; + lookupTableAddress?: PublicKey; + tipAmount?: number; + repayReserve?: ReserveType; + token2022Mint?: string; + repayToken2022Mint?: string; + debug?: boolean; +}; type SupportType = | "wrap" @@ -216,7 +216,7 @@ export class SolendActionCore { token2022Mint?: PublicKey; wrappedAta?: PublicKey; - + environment: EnvironmentType; private constructor( @@ -235,10 +235,10 @@ export class SolendActionCore { depositReserves: Array, borrowReserves: Array, config?: { - environment?: EnvironmentType, - hostAta?: PublicKey, - lookupTableAccount?: AddressLookupTableAccount, - tipAmount?: number, + environment?: EnvironmentType; + hostAta?: PublicKey; + lookupTableAccount?: AddressLookupTableAccount; + tipAmount?: number; repayInfo?: { userRepayTokenAccountAddress: PublicKey; userRepayCollateralAccountAddress: PublicKey; @@ -246,9 +246,9 @@ export class SolendActionCore { repayWrappedAta?: PublicKey; repayMint: PublicKey; reserveAddress: PublicKey; - }, - token2022Mint?: PublicKey, - wrappedAta?: PublicKey, + }; + token2022Mint?: PublicKey; + wrappedAta?: PublicKey; debug?: boolean; } ) { @@ -281,7 +281,7 @@ export class SolendActionCore { this.wrappedAta = config?.wrappedAta; // temporarily default to true this.debug = config?.debug ?? true; - this.environment = config?.environment ?? 'production'; + this.environment = config?.environment ?? "production"; } static async initialize( @@ -294,7 +294,7 @@ export class SolendActionCore { config: ActionConfigType ) { const seed = config.customObligationSeed ?? pool.address.slice(0, 32); - const programId = getProgramId(config.environment ?? 'production'); + const programId = getProgramId(config.environment ?? "production"); const obligationAddress = config.customObligationAddress ?? @@ -356,7 +356,8 @@ export class SolendActionCore { true ); const lookupTableAccount = config.lookupTableAddress - ? (await connection.getAddressLookupTable(config.lookupTableAddress)).value + ? (await connection.getAddressLookupTable(config.lookupTableAddress)) + .value : undefined; const userRepayTokenAccountAddress = config.repayReserve @@ -391,41 +392,44 @@ export class SolendActionCore { depositReserves, borrowReserves, -{ - environment: config.environment, - hostAta: config.hostAta, - lookupTableAccount: lookupTableAccount ?? undefined, - tipAmount: config.tipAmount, - repayInfo: config.repayReserve - ? { - userRepayTokenAccountAddress: userRepayTokenAccountAddress!, - userRepayCollateralAccountAddress: - userRepayCollateralAccountAddress!, - repayToken2022Mint: config.repayToken2022Mint - ? new PublicKey(config.repayToken2022Mint) + { + environment: config.environment, + hostAta: config.hostAta, + lookupTableAccount: lookupTableAccount ?? undefined, + tipAmount: config.tipAmount, + repayInfo: config.repayReserve + ? { + userRepayTokenAccountAddress: userRepayTokenAccountAddress!, + userRepayCollateralAccountAddress: + userRepayCollateralAccountAddress!, + repayToken2022Mint: config.repayToken2022Mint + ? new PublicKey(config.repayToken2022Mint) + : undefined, + repayWrappedAta: config.repayToken2022Mint + ? getAssociatedTokenAddressSync( + new PublicKey(config.repayToken2022Mint), + wallet.publicKey, + true, + TOKEN_2022_PROGRAM_ID + ) + : undefined, + repayMint: new PublicKey(config.repayReserve.mintAddress), + reserveAddress: new PublicKey(config.repayReserve.address), + } + : undefined, + token2022Mint: config.token2022Mint + ? new PublicKey(config.token2022Mint) : undefined, - repayWrappedAta: config.repayToken2022Mint + wrappedAta: config.token2022Mint ? getAssociatedTokenAddressSync( - new PublicKey(config.repayToken2022Mint), + new PublicKey(config.token2022Mint), wallet.publicKey, true, TOKEN_2022_PROGRAM_ID ) : undefined, - repayMint: new PublicKey(config.repayReserve.mintAddress), - reserveAddress: new PublicKey(config.repayReserve.address), } - : undefined, - token2022Mint: config.token2022Mint ? new PublicKey(config.token2022Mint) : undefined, - wrappedAta: config.token2022Mint - ? getAssociatedTokenAddressSync( - new PublicKey(config.token2022Mint), - wallet.publicKey, - true, - TOKEN_2022_PROGRAM_ID - ) - : undefined - }) + ); } static async buildForgiveTxns( @@ -446,7 +450,7 @@ export class SolendActionCore { connection, { ...config, - customObligationAddress: obligationAddress + customObligationAddress: obligationAddress, } ); @@ -772,7 +776,7 @@ export class SolendActionCore { const tip = this.getTipIx(); if (tip && this.pullPriceTxns.length >= 5) { - if (this.debug) console.log('adding tip ix to lending txn'); + if (this.debug) console.log("adding tip ix to lending txn"); instructions.push(tip); } @@ -811,7 +815,7 @@ export class SolendActionCore { } addForgiveIx() { - if (this.debug) console.log('adding forgive ix to lending txn'); + if (this.debug) console.log("adding forgive ix to lending txn"); this.lendingIxs.push( forgiveDebtInstruction( this.obligationAddress, @@ -825,7 +829,7 @@ export class SolendActionCore { } addDepositIx() { - if (this.debug) console.log('adding deposit ix to lending txn'); + if (this.debug) console.log("adding deposit ix to lending txn"); this.lendingIxs.push( this.amount.toString() === U64_MAX ? depositMaxReserveLiquidityAndObligationCollateralInstruction( @@ -865,7 +869,7 @@ export class SolendActionCore { } addDepositReserveLiquidityIx() { - if (this.debug) console.log('adding mint ix to lending txn'); + if (this.debug) console.log("adding mint ix to lending txn"); this.lendingIxs.push( depositReserveLiquidityInstruction( this.amount, @@ -883,7 +887,7 @@ export class SolendActionCore { } addRedeemReserveCollateralIx() { - if (this.debug) console.log('adding redeem ix to lending txn'); + if (this.debug) console.log("adding redeem ix to lending txn"); this.lendingIxs.push( redeemReserveCollateralInstruction( this.amount, @@ -901,7 +905,7 @@ export class SolendActionCore { } async addWithdrawObligationCollateralIx() { - if (this.debug) console.log('adding withdrawCollateral ix to lending txn'); + if (this.debug) console.log("adding withdrawCollateral ix to lending txn"); this.lendingIxs.push( withdrawObligationCollateralInstruction( this.amount, @@ -919,7 +923,7 @@ export class SolendActionCore { } addDepositObligationCollateralIx() { - if (this.debug) console.log('adding depositCollateral ix to lending txn'); + if (this.debug) console.log("adding depositCollateral ix to lending txn"); this.lendingIxs.push( depositObligationCollateralInstruction( this.amount, @@ -936,7 +940,7 @@ export class SolendActionCore { } addBorrowIx() { - if (this.debug) console.log('adding borrow ix to lending txn'); + if (this.debug) console.log("adding borrow ix to lending txn"); this.lendingIxs.push( borrowObligationLiquidityInstruction( this.amount, @@ -956,7 +960,7 @@ export class SolendActionCore { } async addWithdrawIx() { - if (this.debug) console.log('adding withdraw ix to lending txn'); + if (this.debug) console.log("adding withdraw ix to lending txn"); this.lendingIxs.push( this.amount.eq(new BN(U64_MAX)) ? withdrawObligationCollateralAndRedeemReserveLiquidity( @@ -995,7 +999,7 @@ export class SolendActionCore { } async addRepayIx() { - if (this.debug) console.log('adding repay ix to lending txn'); + if (this.debug) console.log("adding repay ix to lending txn"); this.lendingIxs.push( this.amount.toString() === U64_MAX ? repayMaxObligationLiquidityInstruction( @@ -1021,7 +1025,7 @@ export class SolendActionCore { } async addLiquidateIx(repayReserve: ReserveType) { - if (this.debug) console.log('adding liquidate ix to lending txn'); + if (this.debug) console.log("adding liquidate ix to lending txn"); if ( !this.repayInfo?.userRepayCollateralAccountAddress || !this.repayInfo?.userRepayTokenAccountAddress @@ -1095,7 +1099,7 @@ export class SolendActionCore { private async addWrapIx() { if (!this.wrappedAta || !this.token2022Mint) throw new Error("Wrapped ATA not initialized"); - if (this.debug) console.log('adding wrap ix to preTxnIxs'); + if (this.debug) console.log("adding wrap ix to preTxnIxs"); this.preTxnIxs.push( createAssociatedTokenAccountIdempotentInstruction( this.publicKey, @@ -1118,7 +1122,7 @@ export class SolendActionCore { private async addUnwrapIx() { if (!this.wrappedAta || !this.token2022Mint) throw new Error("Wrapped ATA not initialized"); - if (this.debug) console.log('adding wrap ix to preTxnIxs'); + if (this.debug) console.log("adding wrap ix to preTxnIxs"); this.preTxnIxs.push( createAssociatedTokenAccountIdempotentInstruction( this.publicKey, @@ -1129,7 +1133,7 @@ export class SolendActionCore { ) ); - if (this.debug) console.log('adding wrap ix to postTxnIxs'); + if (this.debug) console.log("adding wrap ix to postTxnIxs"); this.postTxnIxs.push( await createWithdrawAndBurnWrapperTokensInstruction( this.publicKey, @@ -1181,7 +1185,9 @@ export class SolendActionCore { if (sbPulledOracles.length) { const feedAccounts = await Promise.all( - sbPulledOracles.map((oracleKey) => new PullFeed(sbod as any, oracleKey)) + sbPulledOracles.map( + (oracleKey) => new PullFeed(sbod as any, oracleKey) + ) ); if (this.debug) console.log("Feed accounts", sbPulledOracles); const loadedFeedAccounts = await Promise.all( @@ -1195,7 +1201,9 @@ export class SolendActionCore { 1 ); - const crossbar = new CrossbarClient("https://crossbar.switchboard.xyz/"); + const crossbar = new CrossbarClient( + "https://crossbar.switchboard.xyz/" + ); const res = sbPulledOracles.reduce((acc, _curr, i) => { if (!(i % 3)) { @@ -1205,55 +1213,57 @@ export class SolendActionCore { return acc; }, [] as string[][]); - await Promise.all( - res.map(async (oracleGroup) => { - const [ix, accountLookups, responses] = await PullFeed.fetchUpdateManyIx(sbod, { + await Promise.all( + res.map(async (oracleGroup) => { + const [ix, accountLookups, responses] = + await PullFeed.fetchUpdateManyIx(sbod, { feeds: oracleGroup.map((p) => new PublicKey(p)), numSignatures, crossbarClient: crossbar, - }) - - if (responses.errors.length) { - console.error(responses.errors); - } + }); - const lookupTables = (await loadLookupTables(feedAccounts)).concat( - accountLookups - ); + if (responses.errors.length) { + console.error(responses.errors); + } - const priorityFeeIx = ComputeBudgetProgram.setComputeUnitPrice({ - microLamports: 1_000_000, - }); - const modifyComputeUnits = ComputeBudgetProgram.setComputeUnitLimit({ - units: 1_000_000, - }); + const lookupTables = (await loadLookupTables(feedAccounts)).concat( + accountLookups + ); - const instructions = [priorityFeeIx, modifyComputeUnits, ix]; + const priorityFeeIx = ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: 1_000_000, + }); + const modifyComputeUnits = ComputeBudgetProgram.setComputeUnitLimit( + { + units: 1_000_000, + } + ); - if (this.debug) console.log('adding tip ix to pullPriceTxns for sbod'); - instructions.push(this.getTipIx()); + const instructions = [priorityFeeIx, modifyComputeUnits, ix]; - // Get the latest context - const { - value: { blockhash }, - } = await this.connection.getLatestBlockhashAndContext(); + if (this.debug) + console.log("adding tip ix to pullPriceTxns for sbod"); + instructions.push(this.getTipIx()); - // Get Transaction Message - const message = new TransactionMessage({ - payerKey: this.publicKey, - recentBlockhash: blockhash, - instructions, - }).compileToV0Message(lookupTables); + // Get the latest context + const { + value: { blockhash }, + } = await this.connection.getLatestBlockhashAndContext(); - // Get Versioned Transaction - const vtx = new VersionedTransaction(message); + // Get Transaction Message + const message = new TransactionMessage({ + payerKey: this.publicKey, + recentBlockhash: blockhash, + instructions, + }).compileToV0Message(lookupTables); - if (this.debug) console.log('adding sbod ix to pullPriceTxns'); - this.pullPriceTxns.push(vtx); - } - - ), - ) + // Get Versioned Transaction + const vtx = new VersionedTransaction(message); + + if (this.debug) console.log("adding sbod ix to pullPriceTxns"); + this.pullPriceTxns.push(vtx); + }) + ); } } @@ -1332,7 +1342,9 @@ export class SolendActionCore { } this.pullPriceTxns.push(tx); } - console.log(`adding ${transactionsWithSigners.length} txns to pullPriceTxns`); + console.log( + `adding ${transactionsWithSigners.length} txns to pullPriceTxns` + ); } } @@ -1370,7 +1382,8 @@ export class SolendActionCore { if (!reserveInfo) { throw new Error(`Could not find asset ${reserveAddress} in reserves`); } - if (this.debug) console.log(`adding refresh ${reserveAddress} ix to setup txn`); + if (this.debug) + console.log(`adding refresh ${reserveAddress} ix to setup txn`); const refreshReserveIx = refreshReserveInstruction( new PublicKey(reserveAddress), this.programId, @@ -1391,19 +1404,20 @@ export class SolendActionCore { this.borrowReserves, this.programId ); - if (this.debug) console.log('adding refresh obligation ix to setup txn'); + if (this.debug) console.log("adding refresh obligation ix to setup txn"); this.setupIxs.push(refreshObligationIx); } private async addObligationIxs() { - if (this.debug) console.log('addObligationIxs'); + if (this.debug) console.log("addObligationIxs"); if (!this.obligationAccountInfo) { const obligationAccountInfoRentExempt = await this.connection.getMinimumBalanceForRentExemption( OBLIGATION_SIZE ); - if (this.debug) console.log('adding createAccount and initObligation ix to setup txn'); + if (this.debug) + console.log("adding createAccount and initObligation ix to setup txn"); this.setupIxs.push( SystemProgram.createAccountWithSeed({ fromPubkey: this.publicKey, @@ -1444,10 +1458,10 @@ export class SolendActionCore { this.hostAta && !this.lookupTableAccount ) { - if (this.debug) console.log('adding createAta ix to pre txn'); + if (this.debug) console.log("adding createAta ix to pre txn"); this.preTxnIxs.push(createUserTokenAccountIx); } else { - if (this.debug) console.log('adding createAta ix to setup txn'); + if (this.debug) console.log("adding createAta ix to setup txn"); this.setupIxs.push(createUserTokenAccountIx); } } @@ -1473,10 +1487,10 @@ export class SolendActionCore { this.hostAta && !this.lookupTableAccount ) { - if (this.debug) console.log('adding createCAta ix to pre txn'); + if (this.debug) console.log("adding createCAta ix to pre txn"); this.preTxnIxs.push(createUserCollateralAccountIx); } else { - if (this.debug) console.log('adding createCAta ix to setup txn'); + if (this.debug) console.log("adding createCAta ix to setup txn"); this.setupIxs.push(createUserCollateralAccountIx); } } @@ -1573,7 +1587,7 @@ export class SolendActionCore { (userWSOLAccountInfo ? 0 : rentExempt) + (sendAction ? parseInt(safeRepay.toString(), 10) : 0), }); - if (this.debug) console.log('adding transferLamports ix'); + if (this.debug) console.log("adding transferLamports ix"); preIxs.push(transferLamportsIx); const closeWSOLAccountIx = createCloseAccountInstruction( @@ -1586,10 +1600,10 @@ export class SolendActionCore { if (userWSOLAccountInfo) { const syncIx = syncNative(solAccountAddress); if (sendAction) { - if (this.debug) console.log('adding syncIx ix'); + if (this.debug) console.log("adding syncIx ix"); preIxs.push(syncIx); } else { - if (this.debug) console.log('adding closeWSOLAccountIx ix'); + if (this.debug) console.log("adding closeWSOLAccountIx ix"); postIxs.push(closeWSOLAccountIx); } } else { @@ -1599,9 +1613,9 @@ export class SolendActionCore { this.publicKey, NATIVE_MINT ); - if (this.debug) console.log('adding createUserWSOLAccountIx ix'); + if (this.debug) console.log("adding createUserWSOLAccountIx ix"); preIxs.push(createUserWSOLAccountIx); - if (this.debug) console.log('adding closeWSOLAccountIx ix'); + if (this.debug) console.log("adding closeWSOLAccountIx ix"); postIxs.push(closeWSOLAccountIx); } @@ -1610,11 +1624,11 @@ export class SolendActionCore { this.hostAta && !this.lookupTableAccount ) { - if (this.debug) console.log('adding above ixs to pre and post txn'); + if (this.debug) console.log("adding above ixs to pre and post txn"); this.preTxnIxs.push(...preIxs); this.postTxnIxs.push(...postIxs); } else { - if (this.debug) console.log('adding above ixs to lending txn'); + if (this.debug) console.log("adding above ixs to lending txn"); this.setupIxs.push(...preIxs); this.cleanupIxs.push(...postIxs); } diff --git a/solend-sdk/src/core/margin.ts b/solend-sdk/src/core/margin.ts index 4cb56526..685f8ceb 100644 --- a/solend-sdk/src/core/margin.ts +++ b/solend-sdk/src/core/margin.ts @@ -415,7 +415,7 @@ export class Margin { environment: "production", customObligationAddress: this.obligationAddress, customObligationSeed: this.obligationSeed, - lookupTableAddress: lookupTableAccount?.key + lookupTableAddress: lookupTableAccount?.key, } ); const { preLendingTxn, lendingTxn } = diff --git a/solend-sdk/src/core/types.ts b/solend-sdk/src/core/types.ts index 46c0a5d9..6d7d4f54 100644 --- a/solend-sdk/src/core/types.ts +++ b/solend-sdk/src/core/types.ts @@ -73,4 +73,4 @@ export type InputReserveConfigParams = { extraOracle?: PublicKey; attributedBorrowLimitOpen: BN; attributedBorrowLimitClose: BN; -} +}; diff --git a/solend-sdk/src/core/utils/obligations.ts b/solend-sdk/src/core/utils/obligations.ts index b812aa0f..35d55dd7 100644 --- a/solend-sdk/src/core/utils/obligations.ts +++ b/solend-sdk/src/core/utils/obligations.ts @@ -10,7 +10,7 @@ export type ObligationType = ReturnType; export function formatObligation( obligation: { pubkey: PublicKey; info: Obligation }, - reserveMap: {[key: string]: ReserveType} + reserveMap: { [key: string]: ReserveType } ) { const poolAddress = obligation.info.lendingMarket.toBase58(); let minPriceUserTotalSupply = new BigNumber(0); diff --git a/solend-sdk/src/core/utils/pools.ts b/solend-sdk/src/core/utils/pools.ts index 4640547d..c7034f2c 100644 --- a/solend-sdk/src/core/utils/pools.ts +++ b/solend-sdk/src/core/utils/pools.ts @@ -66,8 +66,8 @@ export function formatReserve( name?: string; }, config?: { - showApy: boolean, - avgSlotTime: number, + showApy: boolean; + avgSlotTime: number; } ) { const decimals = reserve.info.liquidity.mintDecimals; @@ -101,16 +101,18 @@ export function formatReserve( const lstPatch = priceData?.lstAdjustmentRatio ? { - loanToValueRatio: Number( - new BigNumber(reserve.info.config.loanToValueRatio) - .div(priceData.lstAdjustmentRatio) - .toString(), - ) / 100, - liquidationThreshold: Number( - new BigNumber(reserve.info.config.liquidationThreshold) - .div(priceData.lstAdjustmentRatio) - .toString(), - ) / 100, + loanToValueRatio: + Number( + new BigNumber(reserve.info.config.loanToValueRatio) + .div(priceData.lstAdjustmentRatio) + .toString() + ) / 100, + liquidationThreshold: + Number( + new BigNumber(reserve.info.config.liquidationThreshold) + .div(priceData.lstAdjustmentRatio) + .toString() + ) / 100, } : {}; @@ -146,8 +148,14 @@ export function formatReserve( minBorrowApr: reserve.info.config.minBorrowRate / 100, maxBorrowApr: reserve.info.config.maxBorrowRate / 100, superMaxBorrowRate: reserve.info.config.superMaxBorrowRate.toNumber() / 100, - supplyInterest: calculateSupplyInterest(reserve.info, Boolean(config?.showApy)).times((0.5 / (config?.avgSlotTime ?? 0.5))), - borrowInterest: calculateBorrowInterest(reserve.info, Boolean(config?.showApy)).times((0.5 / (config?.avgSlotTime ?? 0.5))), + supplyInterest: calculateSupplyInterest( + reserve.info, + Boolean(config?.showApy) + ).times(0.5 / (config?.avgSlotTime ?? 0.5)), + borrowInterest: calculateBorrowInterest( + reserve.info, + Boolean(config?.showApy) + ).times(0.5 / (config?.avgSlotTime ?? 0.5)), totalSupply, totalBorrow, availableAmount, @@ -193,7 +201,7 @@ export function formatReserve( attributedBorrowValue: reserve.info.config.attributedBorrowValue, attributedBorrowLimitOpen: reserve.info.config.attributedBorrowLimitOpen, attributedBorrowLimitClose: reserve.info.config.attributedBorrowLimitClose, - ...lstPatch + ...lstPatch, }; } diff --git a/solend-sdk/src/index.ts b/solend-sdk/src/index.ts index 5fefb218..7448f593 100644 --- a/solend-sdk/src/index.ts +++ b/solend-sdk/src/index.ts @@ -1,3 +1,3 @@ export * from "./lib"; export * from "./core"; -export * from "./transaction"; \ No newline at end of file +export * from "./transaction"; diff --git a/solend-sdk/src/rpc/cached.ts b/solend-sdk/src/rpc/cached.ts index 8e0991e4..123702ac 100644 --- a/solend-sdk/src/rpc/cached.ts +++ b/solend-sdk/src/rpc/cached.ts @@ -21,11 +21,15 @@ import { RpcResponseAndContext, SendOptions, SignatureResult, + SignaturesForAddressOptions, + SignatureStatus, + SignatureStatusConfig, SimulatedTransactionResponse, SimulateTransactionConfig, TokenAmount, TransactionResponse, TransactionSignature, + VersionedMessage, VersionedTransaction, VersionedTransactionResponse, } from "@solana/web3.js"; @@ -300,4 +304,79 @@ export class CachedConnection implements SolendRPCConnection { )) ); } + async getSignatureStatus( + signature: TransactionSignature, + config?: SignatureStatusConfig + ): Promise> { + const key = `getSignatureStatus_${signature}_${JSON.stringify(config)}`; + return ( + (await this.cache.get(key)) || + (await this.cache.set( + key, + this.connection.getSignatureStatus(signature, config) + )) + ); + } + + async getSignatureStatuses( + signatures: Array, + config?: SignatureStatusConfig + ): Promise>> { + const key = `getSignatureStatuses_${signatures + .map((sig) => sig) + .join("_")}_${JSON.stringify(config)}`; + return ( + (await this.cache.get(key)) || + (await this.cache.set( + key, + this.connection.getSignatureStatuses(signatures, config) + )) + ); + } + + async getSignaturesForAddress( + address: PublicKey, + options?: SignaturesForAddressOptions, + commitment?: Finality + ): Promise> { + const key = `getSignaturesForAddress_${address.toBase58()}_${JSON.stringify( + options + )}_${commitment}`; + return ( + (await this.cache.get(key)) || + (await this.cache.set( + key, + this.connection.getSignaturesForAddress(address, options, commitment) + )) + ); + } + + async getBlocks( + startSlot: number, + endSlot?: number, + commitment?: Finality + ): Promise> { + const key = `getBlocks_${startSlot}_${endSlot}_${commitment}`; + return ( + (await this.cache.get(key)) || + (await this.cache.set( + key, + this.connection.getBlocks(startSlot, endSlot, commitment) + )) + ); + } + + async getFeeForMessage( + message: VersionedMessage, + commitment?: Commitment + ): Promise> { + const key = `getFeeForMessage_${JSON.stringify(message)}_${commitment}`; + return ( + (await this.cache.get(key)) || + (await this.cache.set( + key, + this.connection.getFeeForMessage(message, commitment) + )) + ); + } } diff --git a/solend-sdk/src/rpc/instrumented.ts b/solend-sdk/src/rpc/instrumented.ts index 89e54336..f70d1125 100644 --- a/solend-sdk/src/rpc/instrumented.ts +++ b/solend-sdk/src/rpc/instrumented.ts @@ -21,11 +21,15 @@ import { RpcResponseAndContext, SendOptions, SignatureResult, + SignaturesForAddressOptions, + SignatureStatus, + SignatureStatusConfig, SimulatedTransactionResponse, SimulateTransactionConfig, TokenAmount, TransactionResponse, TransactionSignature, + VersionedMessage, VersionedTransaction, VersionedTransactionResponse, } from "@solana/web3.js"; @@ -196,6 +200,58 @@ export class InstrumentedConnection implements SolendRPCConnection { ); } + async getSignatureStatus( + signature: TransactionSignature, + config?: SignatureStatusConfig + ): Promise> { + return this.withStats( + this.connection.getSignatureStatus(signature, config), + "getSignatureStatus" + ); + } + + async getSignatureStatuses( + signatures: Array, + config?: SignatureStatusConfig + ): Promise>> { + return this.withStats( + this.connection.getSignatureStatuses(signatures, config), + "getSignatureStatuses" + ); + } + + async getSignaturesForAddress( + address: PublicKey, + options?: SignaturesForAddressOptions, + commitment?: Finality + ): Promise> { + return this.withStats( + this.connection.getSignaturesForAddress(address, options, commitment), + "getSignaturesForAddress" + ); + } + + async getBlocks( + startSlot: number, + endSlot?: number, + commitment?: Finality + ): Promise> { + return this.withStats( + this.connection.getBlocks(startSlot, endSlot, commitment), + "getBlocks" + ); + } + + async getFeeForMessage( + message: VersionedMessage, + commitment?: Commitment + ): Promise> { + return this.withStats( + this.connection.getFeeForMessage(message, commitment), + "getFeeForMessage" + ); + } + async withStats(fn: Promise, fnName: string) { const tags = [`rpc:${this.prefix}`, `function:${fnName}`]; this.statsd.increment("rpc_method_call", tags); diff --git a/solend-sdk/src/rpc/interface.ts b/solend-sdk/src/rpc/interface.ts index f83c7262..7a56e937 100644 --- a/solend-sdk/src/rpc/interface.ts +++ b/solend-sdk/src/rpc/interface.ts @@ -21,11 +21,15 @@ import { RpcResponseAndContext, SendOptions, SignatureResult, + SignaturesForAddressOptions, + SignatureStatus, + SignatureStatusConfig, SimulatedTransactionResponse, SimulateTransactionConfig, TokenAmount, TransactionResponse, TransactionSignature, + VersionedMessage, VersionedTransaction, VersionedTransactionResponse, } from "@solana/web3.js"; @@ -92,4 +96,26 @@ export interface SolendRPCConnection { strategy: BlockheightBasedTransactionConfirmationStrategy, commitment?: Commitment ): Promise>; + getSignatureStatus( + signature: TransactionSignature, + config?: SignatureStatusConfig + ): Promise>; + getSignatureStatuses( + signatures: Array, + config?: SignatureStatusConfig + ): Promise>>; + getSignaturesForAddress( + address: PublicKey, + options?: SignaturesForAddressOptions, + commitment?: Finality + ): Promise>; + getBlocks( + startSlot: number, + endSlot?: number, + commitment?: Finality + ): Promise>; + getFeeForMessage( + message: VersionedMessage, + commitment?: Commitment + ): Promise>; } diff --git a/solend-sdk/src/rpc/multi.ts b/solend-sdk/src/rpc/multi.ts index d96e6e12..0440ba4d 100644 --- a/solend-sdk/src/rpc/multi.ts +++ b/solend-sdk/src/rpc/multi.ts @@ -21,11 +21,15 @@ import { RpcResponseAndContext, SendOptions, SignatureResult, + SignaturesForAddressOptions, + SignatureStatus, + SignatureStatusConfig, SimulatedTransactionResponse, SimulateTransactionConfig, TokenAmount, TransactionResponse, TransactionSignature, + VersionedMessage, VersionedTransaction, VersionedTransactionResponse, } from "@solana/web3.js"; @@ -238,6 +242,78 @@ export class MultiConnection implements SolendRPCConnection { ) ); } + + getSignatureStatus( + signature: TransactionSignature, + config?: SignatureStatusConfig + ): Promise> { + return Promise.race( + this.connections.map((c, index) => + delayed( + index === 0 ? 0 : this.delay, + c.getSignatureStatus(signature, config) + ) + ) + ); + } + + getSignatureStatuses( + signatures: Array, + config?: SignatureStatusConfig + ): Promise>> { + return Promise.race( + this.connections.map((c, index) => + delayed( + index === 0 ? 0 : this.delay, + c.getSignatureStatuses(signatures, config) + ) + ) + ); + } + + getSignaturesForAddress( + address: PublicKey, + options?: SignaturesForAddressOptions, + commitment?: Finality + ): Promise> { + return Promise.race( + this.connections.map((c, index) => + delayed( + index === 0 ? 0 : this.delay, + c.getSignaturesForAddress(address, options, commitment) + ) + ) + ); + } + + getBlocks( + startSlot: number, + endSlot?: number, + commitment?: Finality + ): Promise> { + return Promise.race( + this.connections.map((c, index) => + delayed( + index === 0 ? 0 : this.delay, + c.getBlocks(startSlot, endSlot, commitment) + ) + ) + ); + } + + getFeeForMessage( + message: VersionedMessage, + commitment?: Commitment + ): Promise> { + return Promise.race( + this.connections.map((c, index) => + delayed( + index === 0 ? 0 : this.delay, + c.getFeeForMessage(message, commitment) + ) + ) + ); + } } const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); diff --git a/solend-sdk/src/rpc/retry.ts b/solend-sdk/src/rpc/retry.ts index 577bae50..9fcf8994 100644 --- a/solend-sdk/src/rpc/retry.ts +++ b/solend-sdk/src/rpc/retry.ts @@ -21,11 +21,15 @@ import { RpcResponseAndContext, SendOptions, SignatureResult, + SignaturesForAddressOptions, + SignatureStatus, + SignatureStatusConfig, SimulatedTransactionResponse, SimulateTransactionConfig, TokenAmount, TransactionResponse, TransactionSignature, + VersionedMessage, VersionedTransaction, VersionedTransactionResponse, } from "@solana/web3.js"; @@ -169,6 +173,53 @@ export class RetryConnection implements SolendRPCConnection { ); } + getSignatureStatus( + signature: TransactionSignature, + config?: SignatureStatusConfig + ): Promise> { + return this.withRetries( + this.connection.getSignatureStatus(signature, config) + ); + } + + getSignatureStatuses( + signatures: Array, + config?: SignatureStatusConfig + ): Promise>> { + return this.withRetries( + this.connection.getSignatureStatuses(signatures, config) + ); + } + + getSignaturesForAddress( + address: PublicKey, + options?: SignaturesForAddressOptions, + commitment?: Finality + ): Promise> { + return this.withRetries( + this.connection.getSignaturesForAddress(address, options, commitment) + ); + } + + getBlocks( + startSlot: number, + endSlot?: number, + commitment?: Finality + ): Promise> { + return this.withRetries( + this.connection.getBlocks(startSlot, endSlot, commitment) + ); + } + + getFeeForMessage( + message: VersionedMessage, + commitment?: Commitment + ): Promise> { + return this.withRetries( + this.connection.getFeeForMessage(message, commitment) + ); + } + async withRetries(fn: Promise) { let numTries = 0; let lastException; diff --git a/solend-sdk/src/transaction.ts b/solend-sdk/src/transaction.ts index 929567cd..9c04c54a 100644 --- a/solend-sdk/src/transaction.ts +++ b/solend-sdk/src/transaction.ts @@ -1,137 +1,135 @@ import { - AddressLookupTableAccount, - PACKET_DATA_SIZE, - PublicKey, - Signer, - TransactionInstruction, - } from "@solana/web3.js"; - - /** - * If the transaction doesn't contain a `setComputeUnitLimit` instruction, the default compute budget is 200,000 units per instruction. - */ - export const DEFAULT_COMPUTE_BUDGET_UNITS = 200000; - - /** - * The maximum size of a Solana transaction, leaving some room for the compute budget instructions. - */ - export const PACKET_DATA_SIZE_WITH_ROOM_FOR_COMPUTE_BUDGET = - PACKET_DATA_SIZE - 52; - - /** - * An instruction with some extra information that will be used to build transactions. - */ - export type InstructionWithEphemeralSigners = { - /** The instruction */ - instruction: TransactionInstruction; - /** The ephemeral signers that need to sign the transaction where this instruction will be */ - signers: Signer[]; - /** The compute units that this instruction requires, useful if greater than `DEFAULT_COMPUTE_BUDGET_UNITS` */ - computeUnits?: number; - }; - - /** - * The priority fee configuration for transactions - */ - export type PriorityFeeConfig = { - /** This is the priority fee in micro lamports, it gets passed down to `setComputeUnitPrice` */ - computeUnitPriceMicroLamports?: number; - tightComputeBudget?: boolean; - jitoTipLamports?: number; - jitoBundleSize?: number; - }; - - /** - * A default priority fee configuration. Using a priority fee is helpful even when you're not writing to hot accounts. - */ - export const DEFAULT_PRIORITY_FEE_CONFIG: PriorityFeeConfig = { - computeUnitPriceMicroLamports: 50000, - }; - - /** - * Get the size of a transaction that would contain the provided array of instructions - * This is based on {@link https://solana.com/docs/core/transactions}. - * - * Each transaction has the following layout : - * - * - A compact array of all signatures - * - A 3-bytes message header - * - A compact array with all the account addresses - * - A recent blockhash - * - A compact array of instructions - * - * If the transaction is a `VersionedTransaction`, it also contains an extra byte at the beginning, indicating the version and an array of `MessageAddressTableLookup` at the end. - * After this field there is an array of indexes into the address lookup table that represents the accounts from the address lookup table used in the transaction. - * - * Each instruction has the following layout : - * - One byte indicating the index of the program in the account addresses array - * - A compact array of indices into the account addresses array, indicating which accounts are used by the instruction - * - A compact array of serialized instruction data - */ - export function getSizeOfTransaction( - instructions: TransactionInstruction[], - versionedTransaction = true, - addressLookupTableAddresses?: PublicKey[], - ): number { - const programs = new Set(); - const signers = new Set(); - let accounts = new Set(); - - instructions.map((ix) => { - programs.add(ix.programId.toBase58()); - accounts.add(ix.programId.toBase58()); - ix.keys.map((key) => { - if (key.isSigner) { - signers.add(key.pubkey.toBase58()); - } - accounts.add(key.pubkey.toBase58()); - }); + AddressLookupTableAccount, + PACKET_DATA_SIZE, + PublicKey, + Signer, + TransactionInstruction, +} from "@solana/web3.js"; + +/** + * If the transaction doesn't contain a `setComputeUnitLimit` instruction, the default compute budget is 200,000 units per instruction. + */ +export const DEFAULT_COMPUTE_BUDGET_UNITS = 200000; + +/** + * The maximum size of a Solana transaction, leaving some room for the compute budget instructions. + */ +export const PACKET_DATA_SIZE_WITH_ROOM_FOR_COMPUTE_BUDGET = + PACKET_DATA_SIZE - 52; + +/** + * An instruction with some extra information that will be used to build transactions. + */ +export type InstructionWithEphemeralSigners = { + /** The instruction */ + instruction: TransactionInstruction; + /** The ephemeral signers that need to sign the transaction where this instruction will be */ + signers: Signer[]; + /** The compute units that this instruction requires, useful if greater than `DEFAULT_COMPUTE_BUDGET_UNITS` */ + computeUnits?: number; +}; + +/** + * The priority fee configuration for transactions + */ +export type PriorityFeeConfig = { + /** This is the priority fee in micro lamports, it gets passed down to `setComputeUnitPrice` */ + computeUnitPriceMicroLamports?: number; + tightComputeBudget?: boolean; + jitoTipLamports?: number; + jitoBundleSize?: number; +}; + +/** + * A default priority fee configuration. Using a priority fee is helpful even when you're not writing to hot accounts. + */ +export const DEFAULT_PRIORITY_FEE_CONFIG: PriorityFeeConfig = { + computeUnitPriceMicroLamports: 50000, +}; + +/** + * Get the size of a transaction that would contain the provided array of instructions + * This is based on {@link https://solana.com/docs/core/transactions}. + * + * Each transaction has the following layout : + * + * - A compact array of all signatures + * - A 3-bytes message header + * - A compact array with all the account addresses + * - A recent blockhash + * - A compact array of instructions + * + * If the transaction is a `VersionedTransaction`, it also contains an extra byte at the beginning, indicating the version and an array of `MessageAddressTableLookup` at the end. + * After this field there is an array of indexes into the address lookup table that represents the accounts from the address lookup table used in the transaction. + * + * Each instruction has the following layout : + * - One byte indicating the index of the program in the account addresses array + * - A compact array of indices into the account addresses array, indicating which accounts are used by the instruction + * - A compact array of serialized instruction data + */ +export function getSizeOfTransaction( + instructions: TransactionInstruction[], + versionedTransaction = true, + addressLookupTableAddresses?: PublicKey[] +): number { + const programs = new Set(); + const signers = new Set(); + let accounts = new Set(); + + instructions.map((ix) => { + programs.add(ix.programId.toBase58()); + accounts.add(ix.programId.toBase58()); + ix.keys.map((key) => { + if (key.isSigner) { + signers.add(key.pubkey.toBase58()); + } + accounts.add(key.pubkey.toBase58()); }); - - const instruction_sizes: number = instructions - .map( - (ix) => - 1 + - getSizeOfCompressedU16(ix.keys.length) + - ix.keys.length + - getSizeOfCompressedU16(ix.data.length) + - ix.data.length - ) - .reduce((a, b) => a + b, 0); - - let numberOfAddressLookups = 0; - if (addressLookupTableAddresses) { - const lookupTableAddresses = addressLookupTableAddresses.map( - (address) => address.toBase58() - ); - const totalNumberOfAccounts = accounts.size; - accounts = new Set( - [...accounts].filter((account) => !lookupTableAddresses.includes(account)) - ); - accounts = new Set([...accounts, ...programs, ...signers]); - numberOfAddressLookups = totalNumberOfAccounts - accounts.size; // This number is equal to the number of accounts that are in the lookup table and are neither signers nor programs - } - - return ( - getSizeOfCompressedU16(signers.size) + - signers.size * 64 + // array of signatures - 3 + - getSizeOfCompressedU16(accounts.size) + - 32 * accounts.size + // array of account addresses - 32 + // recent blockhash - getSizeOfCompressedU16(instructions.length) + - instruction_sizes + // array of instructions - (versionedTransaction ? 1 + getSizeOfCompressedU16(0) : 0) + // transaction version and number of address lookup tables - (versionedTransaction && addressLookupTableAddresses ? 32 : 0) + // address lookup table address (we only support 1 address lookup table) - (versionedTransaction && addressLookupTableAddresses ? 2 : 0) + // number of address lookup indexes - numberOfAddressLookups // address lookup indexes + }); + + const instruction_sizes: number = instructions + .map( + (ix) => + 1 + + getSizeOfCompressedU16(ix.keys.length) + + ix.keys.length + + getSizeOfCompressedU16(ix.data.length) + + ix.data.length + ) + .reduce((a, b) => a + b, 0); + + let numberOfAddressLookups = 0; + if (addressLookupTableAddresses) { + const lookupTableAddresses = addressLookupTableAddresses.map((address) => + address.toBase58() ); + const totalNumberOfAccounts = accounts.size; + accounts = new Set( + [...accounts].filter((account) => !lookupTableAddresses.includes(account)) + ); + accounts = new Set([...accounts, ...programs, ...signers]); + numberOfAddressLookups = totalNumberOfAccounts - accounts.size; // This number is equal to the number of accounts that are in the lookup table and are neither signers nor programs } - - /** - * Get the size of n in bytes when serialized as a CompressedU16. Compact arrays use a CompactU16 to store the length of the array. - */ - export function getSizeOfCompressedU16(n: number) { - return 1 + Number(n >= 128) + Number(n >= 16384); - } - - \ No newline at end of file + + return ( + getSizeOfCompressedU16(signers.size) + + signers.size * 64 + // array of signatures + 3 + + getSizeOfCompressedU16(accounts.size) + + 32 * accounts.size + // array of account addresses + 32 + // recent blockhash + getSizeOfCompressedU16(instructions.length) + + instruction_sizes + // array of instructions + (versionedTransaction ? 1 + getSizeOfCompressedU16(0) : 0) + // transaction version and number of address lookup tables + (versionedTransaction && addressLookupTableAddresses ? 32 : 0) + // address lookup table address (we only support 1 address lookup table) + (versionedTransaction && addressLookupTableAddresses ? 2 : 0) + // number of address lookup indexes + numberOfAddressLookups // address lookup indexes + ); +} + +/** + * Get the size of n in bytes when serialized as a CompressedU16. Compact arrays use a CompactU16 to store the length of the array. + */ +export function getSizeOfCompressedU16(n: number) { + return 1 + Number(n >= 128) + Number(n >= 16384); +} diff --git a/yarn.lock b/yarn.lock index ab5f78fd..9a119710 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1869,6 +1869,7 @@ minimist "^1.2.0" "@coral-xyz/anchor-30@npm:@coral-xyz/anchor@0.30.1", "@coral-xyz/anchor@^0.30.1": + name "@coral-xyz/anchor-30" version "0.30.1" resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.30.1.tgz#17f3e9134c28cd0ea83574c6bab4e410bcecec5d" integrity sha512-gDXFoF5oHgpriXAaLpxyWBHdCs8Awgf/gLHIo6crv7Aqm937CNdY+x+6hoj7QR5vaJV7MxWSQ0NGFzL3kPbWEQ== @@ -5917,10 +5918,10 @@ dependencies: tslib "^2.4.0" -"@switchboard-xyz/common@^2.4.2": - version "2.4.3" - resolved "https://registry.yarnpkg.com/@switchboard-xyz/common/-/common-2.4.3.tgz#3411374cafa230c010b03a5669ddace8a46f3e17" - integrity sha512-0Ucvk753Co48qcWBRRME28zcYIhQgZgIuFUE5kXxq7zSf1k9dFVUOwgcoQVr6LhsK3YAvJdGo1n8JzcxEzCBZQ== +"@switchboard-xyz/common@^2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@switchboard-xyz/common/-/common-2.5.0.tgz#15f60abd0c2503d855caf6f120ed98f8f0dd9d25" + integrity sha512-BgQTvqOZGZxMSscy2x3GHrs2/BRhFf55t7QrqVdED1KneCP0KgcZn436GjGttc61nP2i8Yd7VoJzTNksezn2kA== dependencies: "@solana/web3.js" "^1.93.0" axios "^1.7.2" @@ -5929,21 +5930,21 @@ bs58 "^5.0.0" cron-validator "^1.3.1" decimal.js "^10.4.3" - form-data "^4.0.0" + js-sha256 "^0.11.0" lodash "^4.17.21" protobufjs "^7.2.6" - yaml "^2.2.1" + yaml "^2.5.0" -"@switchboard-xyz/on-demand@1.2.27": - version "1.2.27" - resolved "https://registry.yarnpkg.com/@switchboard-xyz/on-demand/-/on-demand-1.2.27.tgz#8591d2a424a7a9a9256a996df35f408c0167fe40" - integrity sha512-acrIngoQRv6M6PiNooEFQP+0FExseKkCMecGDP1nLRz7C1hLMTgWWeUqmmfrxJh6uCafM4Cl4lpKk3pGd01XBg== +"@switchboard-xyz/on-demand@^1.2.43": + version "1.2.43" + resolved "https://registry.yarnpkg.com/@switchboard-xyz/on-demand/-/on-demand-1.2.43.tgz#bb96a1cb19108dec89c8e7aa82c20aa417a31761" + integrity sha512-YjkGqh2cMZHbDecnfAU/s6MCc9mgm9zcA9y8ti7w11vNBYM1RFspNzwQdmRurL8LVJWAhuJqiE2WGDgLewT1Mg== dependencies: "@brokerloop/ttlcache" "^3.2.3" "@coral-xyz/anchor-30" "npm:@coral-xyz/anchor@0.30.1" "@solana/web3.js" "^1.95.0" "@solworks/soltoolkit-sdk" "^0.0.23" - "@switchboard-xyz/common" "^2.4.2" + "@switchboard-xyz/common" "^2.5.0" axios "^1.7.4" big.js "^6.2.1" bs58 "^5.0.0" @@ -12730,6 +12731,11 @@ js-base64@^3.7.5: resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.7.tgz#e51b84bf78fbf5702b9541e2cb7bfcb893b43e79" integrity sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw== +js-sha256@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.11.0.tgz#256a921d9292f7fe98905face82e367abaca9576" + integrity sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q== + js-sha256@^0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966" @@ -17119,10 +17125,10 @@ yaml@^1.10.0: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== -yaml@^2.2.1: - version "2.4.5" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.5.tgz#60630b206dd6d84df97003d33fc1ddf6296cca5e" - integrity sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg== +yaml@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.1.tgz#c9772aacf62cb7494a95b0c4f1fb065b563db130" + integrity sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q== yargs-parser@20.x: version "20.2.9"