@exponent-labs/exponent-sdk 0.9.0 → 0.9.1
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/build/client/vaults/index.d.ts +2 -0
- package/build/client/vaults/index.js +2 -0
- package/build/client/vaults/index.js.map +1 -1
- package/build/client/vaults/types/index.d.ts +2 -0
- package/build/client/vaults/types/index.js +2 -0
- package/build/client/vaults/types/index.js.map +1 -1
- package/build/client/vaults/types/kaminoFarmEntry.d.ts +15 -0
- package/build/client/vaults/types/kaminoFarmEntry.js +17 -0
- package/build/client/vaults/types/kaminoFarmEntry.js.map +1 -0
- package/build/client/vaults/types/kaminoObligationEntry.d.ts +24 -7
- package/build/client/vaults/types/kaminoObligationEntry.js +2 -1
- package/build/client/vaults/types/kaminoObligationEntry.js.map +1 -1
- package/build/client/vaults/types/obligationType.d.ts +1 -1
- package/build/client/vaults/types/positionUpdate.d.ts +9 -0
- package/build/client/vaults/types/positionUpdate.js +23 -0
- package/build/client/vaults/types/positionUpdate.js.map +1 -1
- package/build/client/vaults/types/proposalAction.d.ts +54 -54
- package/build/client/vaults/types/proposalAction.js +0 -3
- package/build/client/vaults/types/proposalAction.js.map +1 -1
- package/build/client/vaults/types/reserveFarmMapping.d.ts +19 -0
- package/build/client/vaults/types/reserveFarmMapping.js +18 -0
- package/build/client/vaults/types/reserveFarmMapping.js.map +1 -0
- package/build/client/vaults/types/strategyPosition.d.ts +6 -1
- package/build/client/vaults/types/strategyPosition.js +5 -0
- package/build/client/vaults/types/strategyPosition.js.map +1 -1
- package/build/exponentVaults/aumCalculator.d.ts +25 -4
- package/build/exponentVaults/aumCalculator.js +236 -15
- package/build/exponentVaults/aumCalculator.js.map +1 -1
- package/build/exponentVaults/fetcher.d.ts +52 -0
- package/build/exponentVaults/fetcher.js +199 -0
- package/build/exponentVaults/fetcher.js.map +1 -0
- package/build/exponentVaults/index.d.ts +10 -9
- package/build/exponentVaults/index.js +25 -8
- package/build/exponentVaults/index.js.map +1 -1
- package/build/exponentVaults/kamino-farms.d.ts +144 -0
- package/build/exponentVaults/kamino-farms.js +396 -0
- package/build/exponentVaults/kamino-farms.js.map +1 -0
- package/build/exponentVaults/loopscale/client.d.ts +240 -0
- package/build/exponentVaults/loopscale/client.js +590 -0
- package/build/exponentVaults/loopscale/client.js.map +1 -0
- package/build/exponentVaults/loopscale/client.test.d.ts +1 -0
- package/build/exponentVaults/loopscale/client.test.js +183 -0
- package/build/exponentVaults/loopscale/client.test.js.map +1 -0
- package/build/exponentVaults/loopscale/helpers.d.ts +29 -0
- package/build/exponentVaults/loopscale/helpers.js +119 -0
- package/build/exponentVaults/loopscale/helpers.js.map +1 -0
- package/build/exponentVaults/loopscale/index.d.ts +3 -0
- package/build/exponentVaults/loopscale/index.js +12 -0
- package/build/exponentVaults/loopscale/index.js.map +1 -0
- package/build/exponentVaults/loopscale/prepared-transactions.d.ts +13 -0
- package/build/exponentVaults/loopscale/prepared-transactions.js +271 -0
- package/build/exponentVaults/loopscale/prepared-transactions.js.map +1 -0
- package/build/exponentVaults/loopscale/prepared-transactions.test.d.ts +1 -0
- package/build/exponentVaults/loopscale/prepared-transactions.test.js +400 -0
- package/build/exponentVaults/loopscale/prepared-transactions.test.js.map +1 -0
- package/build/exponentVaults/loopscale/prepared-types.d.ts +62 -0
- package/build/exponentVaults/loopscale/prepared-types.js +3 -0
- package/build/exponentVaults/loopscale/prepared-types.js.map +1 -0
- package/build/exponentVaults/loopscale/response-plan.d.ts +69 -0
- package/build/exponentVaults/loopscale/response-plan.js +141 -0
- package/build/exponentVaults/loopscale/response-plan.js.map +1 -0
- package/build/exponentVaults/loopscale/response-plan.test.d.ts +1 -0
- package/build/exponentVaults/loopscale/response-plan.test.js +139 -0
- package/build/exponentVaults/loopscale/response-plan.test.js.map +1 -0
- package/build/exponentVaults/loopscale/send-plan.d.ts +75 -0
- package/build/exponentVaults/loopscale/send-plan.js +235 -0
- package/build/exponentVaults/loopscale/send-plan.js.map +1 -0
- package/build/exponentVaults/loopscale/types.d.ts +443 -0
- package/build/exponentVaults/loopscale/types.js +3 -0
- package/build/exponentVaults/loopscale/types.js.map +1 -0
- package/build/exponentVaults/loopscale-client.d.ts +113 -524
- package/build/exponentVaults/loopscale-client.js +296 -539
- package/build/exponentVaults/loopscale-client.js.map +1 -1
- package/build/exponentVaults/loopscale-client.test.d.ts +1 -0
- package/build/exponentVaults/loopscale-client.test.js +162 -0
- package/build/exponentVaults/loopscale-client.test.js.map +1 -0
- package/build/exponentVaults/loopscale-client.types.d.ts +425 -0
- package/build/exponentVaults/loopscale-client.types.js +3 -0
- package/build/exponentVaults/loopscale-client.types.js.map +1 -0
- package/build/exponentVaults/loopscale-execution.d.ts +125 -0
- package/build/exponentVaults/loopscale-execution.js +341 -0
- package/build/exponentVaults/loopscale-execution.js.map +1 -0
- package/build/exponentVaults/loopscale-execution.test.d.ts +1 -0
- package/build/exponentVaults/loopscale-execution.test.js +139 -0
- package/build/exponentVaults/loopscale-execution.test.js.map +1 -0
- package/build/exponentVaults/loopscale-vault.d.ts +115 -0
- package/build/exponentVaults/loopscale-vault.js +275 -0
- package/build/exponentVaults/loopscale-vault.js.map +1 -0
- package/build/exponentVaults/loopscale-vault.test.d.ts +1 -0
- package/build/exponentVaults/loopscale-vault.test.js +102 -0
- package/build/exponentVaults/loopscale-vault.test.js.map +1 -0
- package/build/exponentVaults/policyBuilders.d.ts +62 -0
- package/build/exponentVaults/policyBuilders.js +119 -2
- package/build/exponentVaults/policyBuilders.js.map +1 -1
- package/build/exponentVaults/pricePathResolver.d.ts +45 -0
- package/build/exponentVaults/pricePathResolver.js +198 -0
- package/build/exponentVaults/pricePathResolver.js.map +1 -0
- package/build/exponentVaults/pricePathResolver.test.d.ts +1 -0
- package/build/exponentVaults/pricePathResolver.test.js +369 -0
- package/build/exponentVaults/pricePathResolver.test.js.map +1 -0
- package/build/exponentVaults/syncTransaction.js +4 -1
- package/build/exponentVaults/syncTransaction.js.map +1 -1
- package/build/exponentVaults/titan-quote.js +170 -36
- package/build/exponentVaults/titan-quote.js.map +1 -1
- package/build/exponentVaults/vault-instruction-types.d.ts +363 -0
- package/build/exponentVaults/vault-instruction-types.js +128 -0
- package/build/exponentVaults/vault-instruction-types.js.map +1 -0
- package/build/exponentVaults/vault-interaction.d.ts +156 -313
- package/build/exponentVaults/vault-interaction.js +1581 -353
- package/build/exponentVaults/vault-interaction.js.map +1 -1
- package/build/exponentVaults/vault.d.ts +51 -2
- package/build/exponentVaults/vault.js +324 -48
- package/build/exponentVaults/vault.js.map +1 -1
- package/build/exponentVaults/vaultTransactionBuilder.d.ts +100 -134
- package/build/exponentVaults/vaultTransactionBuilder.js +359 -266
- package/build/exponentVaults/vaultTransactionBuilder.js.map +1 -1
- package/build/exponentVaults/vaultTransactionBuilder.test.d.ts +1 -0
- package/build/exponentVaults/vaultTransactionBuilder.test.js +214 -0
- package/build/exponentVaults/vaultTransactionBuilder.test.js.map +1 -0
- package/build/marketThree.d.ts +6 -2
- package/build/marketThree.js +10 -8
- package/build/marketThree.js.map +1 -1
- package/package.json +32 -32
- package/src/client/vaults/index.ts +2 -0
- package/src/client/vaults/types/index.ts +2 -0
- package/src/client/vaults/types/kaminoFarmEntry.ts +32 -0
- package/src/client/vaults/types/kaminoObligationEntry.ts +6 -3
- package/src/client/vaults/types/positionUpdate.ts +62 -0
- package/src/client/vaults/types/proposalAction.ts +0 -3
- package/src/client/vaults/types/reserveFarmMapping.ts +35 -0
- package/src/client/vaults/types/strategyPosition.ts +18 -1
- package/src/exponentVaults/aumCalculator.ts +353 -16
- package/src/exponentVaults/fetcher.ts +257 -0
- package/src/exponentVaults/index.ts +64 -40
- package/src/exponentVaults/kamino-farms.ts +538 -0
- package/src/exponentVaults/loopscale/client.ts +808 -0
- package/src/exponentVaults/loopscale/helpers.ts +172 -0
- package/src/exponentVaults/loopscale/index.ts +57 -0
- package/src/exponentVaults/loopscale/prepared-transactions.ts +435 -0
- package/src/exponentVaults/loopscale/prepared-types.ts +73 -0
- package/src/exponentVaults/loopscale/types.ts +466 -0
- package/src/exponentVaults/policyBuilders.ts +170 -0
- package/src/exponentVaults/pricePathResolver.test.ts +466 -0
- package/src/exponentVaults/pricePathResolver.ts +273 -0
- package/src/exponentVaults/syncTransaction.ts +6 -1
- package/src/exponentVaults/titan-quote.ts +231 -45
- package/src/exponentVaults/vault-instruction-types.ts +493 -0
- package/src/exponentVaults/vault-interaction.ts +2227 -636
- package/src/exponentVaults/vault.ts +474 -63
- package/src/exponentVaults/vaultTransactionBuilder.test.ts +256 -0
- package/src/exponentVaults/vaultTransactionBuilder.ts +555 -413
- package/src/marketThree.ts +14 -6
- package/src/exponentVaults/loopscale-client.ts +0 -1373
|
@@ -1,1373 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type AccountMeta,
|
|
3
|
-
type Commitment,
|
|
4
|
-
type Finality,
|
|
5
|
-
type SendOptions,
|
|
6
|
-
type Signer,
|
|
7
|
-
ComputeBudgetProgram,
|
|
8
|
-
Connection,
|
|
9
|
-
PublicKey,
|
|
10
|
-
SystemProgram,
|
|
11
|
-
TransactionInstruction,
|
|
12
|
-
TransactionMessage,
|
|
13
|
-
VersionedMessage,
|
|
14
|
-
VersionedTransaction,
|
|
15
|
-
} from "@solana/web3.js"
|
|
16
|
-
|
|
17
|
-
import { LOOPSCALE_DISCRIMINATORS, LOOPSCALE_PROGRAM_ID } from "./policyBuilders"
|
|
18
|
-
import { type LoopscaleInstruction, LoopscaleAction, loopscaleAction, createVaultSyncTransaction } from "./vault-interaction"
|
|
19
|
-
|
|
20
|
-
// ============================================================================
|
|
21
|
-
// Constants
|
|
22
|
-
// ============================================================================
|
|
23
|
-
|
|
24
|
-
const LOOPSCALE_API_BASE_URL = "https://tars.loopscale.com/v1"
|
|
25
|
-
const BS_AUTH = new PublicKey("CyNKPfqsSLAejjZtEeNG3pR4SkPhSPHXdGhuNTyudrNs")
|
|
26
|
-
const ASSOCIATED_TOKEN_PROGRAM_ID = new PublicKey("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL")
|
|
27
|
-
|
|
28
|
-
// ============================================================================
|
|
29
|
-
// Types — Config
|
|
30
|
-
// ============================================================================
|
|
31
|
-
|
|
32
|
-
export interface LoopscaleClientConfig {
|
|
33
|
-
connection: Connection
|
|
34
|
-
/** Wallet address sent as `user-wallet` / `payer` header to the Loopscale API. */
|
|
35
|
-
userWallet: PublicKey | string
|
|
36
|
-
/** Loopscale API base URL (default: https://tars.loopscale.com/v1). */
|
|
37
|
-
baseUrl?: string
|
|
38
|
-
/** Known bs_auth public key (default: CyNKPf...). */
|
|
39
|
-
bsAuth?: PublicKey
|
|
40
|
-
/** Use Luke's batch-based MPC endpoint for co-signing (default: true). */
|
|
41
|
-
useMpcCoSign?: boolean
|
|
42
|
-
/** Log request/response details to console. */
|
|
43
|
-
debug?: boolean
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// ============================================================================
|
|
47
|
-
// Types — API params
|
|
48
|
-
// ============================================================================
|
|
49
|
-
|
|
50
|
-
export interface LoopscaleQuoteParams {
|
|
51
|
-
durationType: number
|
|
52
|
-
duration: number
|
|
53
|
-
principal: PublicKey | string
|
|
54
|
-
collateral: (PublicKey | string)[]
|
|
55
|
-
limit?: number
|
|
56
|
-
offset?: number
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface LoopscaleCreateStrategyParams {
|
|
60
|
-
principalMint: PublicKey | string
|
|
61
|
-
lender: PublicKey | string
|
|
62
|
-
amount: bigint | number
|
|
63
|
-
originationsEnabled?: boolean
|
|
64
|
-
liquidityBuffer?: number
|
|
65
|
-
interestFee?: number
|
|
66
|
-
originationFee?: number
|
|
67
|
-
originationCap?: number
|
|
68
|
-
collateralTerms?: Array<{
|
|
69
|
-
apy: number
|
|
70
|
-
indices: Array<{ collateralIndex: number; durationIndex: number }>
|
|
71
|
-
}>
|
|
72
|
-
marketInformation?: PublicKey | string
|
|
73
|
-
externalYieldSourceArgs?: TxnExternalYieldSourceArgs
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface LoopscaleDepositStrategyParams {
|
|
77
|
-
strategy: PublicKey | string
|
|
78
|
-
amount: bigint | number
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export interface LoopscaleWithdrawStrategyParams {
|
|
82
|
-
strategy: PublicKey | string
|
|
83
|
-
amount: bigint | number
|
|
84
|
-
withdrawAll: boolean
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export interface LoopscaleCloseStrategyParams {
|
|
88
|
-
strategy: PublicKey | string
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export interface ExternalYieldSourceParams {
|
|
92
|
-
newExternalYieldSource: number
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export interface TxnExternalYieldSourceArgs extends ExternalYieldSourceParams {
|
|
96
|
-
createExternalYieldAccount: boolean
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export interface StrategyDuration {
|
|
100
|
-
duration: number
|
|
101
|
-
durationType: number
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export interface AddCollateralArgs {
|
|
105
|
-
durationsAndApys?: Record<string, string>
|
|
106
|
-
externalMarketInformationAddress?: string
|
|
107
|
-
marketInformation?: Record<string, unknown>
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export interface RemoveCollateralArgs {
|
|
111
|
-
durations: StrategyDuration[]
|
|
112
|
-
removeFromMarketInformation: boolean
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export interface CollateralParamsUpdateArgs {
|
|
116
|
-
ltvUpdate?: Record<string, unknown>
|
|
117
|
-
apyUpdate?: Record<string, string>
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export interface CollateralAllocationUpdateArgs {
|
|
121
|
-
assetIdentifier: string
|
|
122
|
-
maxAllocationPct: string
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export interface StrategyCollateralUpdates {
|
|
126
|
-
addCollateral?: Record<string, AddCollateralArgs>
|
|
127
|
-
removeCollateral?: Record<string, RemoveCollateralArgs>
|
|
128
|
-
updateCollateral?: Record<string, CollateralParamsUpdateArgs>
|
|
129
|
-
updateAssetAllocation?: CollateralAllocationUpdateArgs[]
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export interface EditStrategySettingsArgs {
|
|
133
|
-
originationsEnabled?: boolean
|
|
134
|
-
liquidityBuffer?: number
|
|
135
|
-
interestFee?: number
|
|
136
|
-
originationFee?: number
|
|
137
|
-
principalFee?: number
|
|
138
|
-
originationCap?: number
|
|
139
|
-
externalYieldSource?: ExternalYieldSourceParams
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export interface LoopscaleUpdateStrategyParams {
|
|
143
|
-
strategy: PublicKey | string
|
|
144
|
-
collateralTerms?: StrategyCollateralUpdates
|
|
145
|
-
updateParams?: EditStrategySettingsArgs
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export interface LoopscaleBorrowPrincipalParams {
|
|
149
|
-
/** Borrower / vault address (also the payer for ATA creation). */
|
|
150
|
-
borrower: PublicKey | string
|
|
151
|
-
/** Existing loan with collateral already deposited. */
|
|
152
|
-
loan: PublicKey | string
|
|
153
|
-
/** Strategy supplying principal liquidity. */
|
|
154
|
-
strategy: PublicKey | string
|
|
155
|
-
/** MarketInformation account for the borrow strategy. */
|
|
156
|
-
marketInformation: PublicKey | string
|
|
157
|
-
/** Mint of the borrowed asset. */
|
|
158
|
-
principalMint: PublicKey | string
|
|
159
|
-
/** Amount of principal to borrow (lamports). */
|
|
160
|
-
amount: bigint | number
|
|
161
|
-
/** Duration index (0=1day, 1=1week, 2=1month, 3=3months, 4=5min). */
|
|
162
|
-
durationIndex: number
|
|
163
|
-
/** Byte buffer consumed by sync_risk_matrices + validate_loan_health. */
|
|
164
|
-
assetIndexGuidance: number[]
|
|
165
|
-
/** Oracle remaining accounts for health check (MarketInformation + oracle pubkeys). */
|
|
166
|
-
healthCheckAccounts: AccountMeta[]
|
|
167
|
-
expectedApy?: bigint | number
|
|
168
|
-
/** Expected liquidation thresholds per collateral index [5 entries]. */
|
|
169
|
-
expectedLqt?: number[]
|
|
170
|
-
/** Keep result as wSOL instead of unwrapping (default: false). */
|
|
171
|
-
skipSolUnwrap?: boolean
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export interface LoopscaleRepayLoanParams {
|
|
175
|
-
loan: PublicKey | string
|
|
176
|
-
repayParams: Array<{
|
|
177
|
-
amount: bigint | number
|
|
178
|
-
ledgerIndex: number
|
|
179
|
-
repayAll: boolean
|
|
180
|
-
}>
|
|
181
|
-
collateralWithdrawalParams?: Array<{
|
|
182
|
-
amount: bigint | number
|
|
183
|
-
collateralMint: PublicKey | string
|
|
184
|
-
}>
|
|
185
|
-
closeIfPossible?: boolean
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export interface LoopscaleCloseLoanParams {
|
|
189
|
-
loan: PublicKey | string
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export interface LoopscaleCreateLoanParams {
|
|
193
|
-
borrower: PublicKey | string
|
|
194
|
-
depositCollateral: Array<{
|
|
195
|
-
collateralAmount: bigint | number
|
|
196
|
-
collateralAssetData: { Spl: { mint: string } } | { Orca: { positionMint: string; whirlpool: string } }
|
|
197
|
-
}>
|
|
198
|
-
principalRequested: Array<{
|
|
199
|
-
ledgerIndex: number
|
|
200
|
-
principalAmount: bigint | number
|
|
201
|
-
principalMint: PublicKey | string
|
|
202
|
-
strategy: PublicKey | string
|
|
203
|
-
durationIndex: number
|
|
204
|
-
expectedLoanValues?: { expectedApy?: number; expectedLqt?: number[] }
|
|
205
|
-
}>
|
|
206
|
-
assetIndexGuidance?: number[]
|
|
207
|
-
loanNonce?: string
|
|
208
|
-
isLoop?: boolean
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export interface LoopscaleApiBorrowPrincipalParams {
|
|
212
|
-
loan: PublicKey | string
|
|
213
|
-
borrowParams: {
|
|
214
|
-
amount: bigint | number
|
|
215
|
-
durationIndex: number
|
|
216
|
-
expectedLoanValues?: { expectedApy?: number; expectedLqt?: number[] }
|
|
217
|
-
}
|
|
218
|
-
strategy: PublicKey | string
|
|
219
|
-
refinanceParams?: { ledgerIndex: number; durationIndex: number }
|
|
220
|
-
isLoop?: boolean
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export interface LoopscaleDepositCollateralParams {
|
|
224
|
-
loan: PublicKey | string
|
|
225
|
-
depositMint: PublicKey | string
|
|
226
|
-
amount: bigint | number
|
|
227
|
-
assetType: number
|
|
228
|
-
assetIdentifier: PublicKey | string
|
|
229
|
-
assetIndexGuidance?: number[]
|
|
230
|
-
expectedLoanValues?: { expectedApy?: number; expectedLqt?: number[] }
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export interface LoopscaleWithdrawCollateralParams {
|
|
234
|
-
loan: PublicKey | string
|
|
235
|
-
collateralMint: PublicKey | string
|
|
236
|
-
amount: bigint | number
|
|
237
|
-
collateralIndex: number
|
|
238
|
-
expectedLoanValues?: { expectedApy?: number; expectedLqt?: number[] }
|
|
239
|
-
assetIndexGuidance?: number[]
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export interface LoopscaleRefinanceLoanParams {
|
|
243
|
-
loan: PublicKey | string
|
|
244
|
-
oldStrategy: PublicKey | string
|
|
245
|
-
newStrategy: PublicKey | string
|
|
246
|
-
refinanceParams: { ledgerIndex: number; durationIndex: number }
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// ── Data query types ──
|
|
250
|
-
|
|
251
|
-
export interface LoopscaleMaxQuoteParams {
|
|
252
|
-
durationType: number
|
|
253
|
-
duration: number
|
|
254
|
-
principalMint: PublicKey | string
|
|
255
|
-
collateralFilter: Array<{
|
|
256
|
-
amount: bigint | number
|
|
257
|
-
assetData: { Spl: { mint: string } } | { Orca: { positionMint: string; whirlpool: string } }
|
|
258
|
-
}>
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export interface LoopscaleMaxQuote {
|
|
262
|
-
apy: number
|
|
263
|
-
strategy: string
|
|
264
|
-
collateralIdentifier: string
|
|
265
|
-
ltv: number
|
|
266
|
-
lqt: number
|
|
267
|
-
amount: number
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
export interface LoopscaleLoanInfoParams {
|
|
271
|
-
loanAddresses?: string[]
|
|
272
|
-
lenders?: string[]
|
|
273
|
-
borrowers?: string[]
|
|
274
|
-
filterType?: number
|
|
275
|
-
principalMints?: string[]
|
|
276
|
-
collateralMints?: string[]
|
|
277
|
-
orderFundingType?: number
|
|
278
|
-
page?: number
|
|
279
|
-
pageSize?: number
|
|
280
|
-
sortDirection?: number
|
|
281
|
-
sortType?: number
|
|
282
|
-
assetTypes?: number
|
|
283
|
-
excludeCollateralIdentifiers?: string[]
|
|
284
|
-
excludePrincipalMints?: string[]
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
export interface LoopscaleLoanInfoResponse {
|
|
288
|
-
totalCount: number
|
|
289
|
-
loanInfos: Array<{
|
|
290
|
-
loan: { address: string; loanStatus: number; borrower: string; startTime: number; [key: string]: unknown }
|
|
291
|
-
loanType: number
|
|
292
|
-
ledgers: Array<{ ledgerIndex: number; strategy: string; principalMint: string; principalDue: number; interestOutstanding: number; apy: number; endTime: number; [key: string]: unknown }>
|
|
293
|
-
collateral: Array<{ assetMint: string; amount: number; assetType: number; [key: string]: unknown }>
|
|
294
|
-
[key: string]: unknown
|
|
295
|
-
}>
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
export interface LoopscaleGetStrategiesParams {
|
|
299
|
-
userAddress?: string
|
|
300
|
-
addresses?: string[]
|
|
301
|
-
principalMints?: string[]
|
|
302
|
-
page?: number
|
|
303
|
-
pageSize?: number
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
export interface LoopscaleStrategyInfoResponse {
|
|
307
|
-
strategies: Array<{
|
|
308
|
-
strategy: { address: string; principalMint: string; tokenBalance: number; currentDeployedAmount: number; outstandingInterestAmount: number; [key: string]: unknown }
|
|
309
|
-
externalYieldInfo?: { apy: number }
|
|
310
|
-
}>
|
|
311
|
-
total: number
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
export interface LoopscaleCollateralHoldersParams {
|
|
315
|
-
mints: string[]
|
|
316
|
-
pdas?: boolean
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
export interface LoopscaleCollateralHolder {
|
|
320
|
-
collateralMint: string
|
|
321
|
-
totalDeposits: number
|
|
322
|
-
userDeposits: Record<string, number>
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
export interface LoopscaleHistoricalCollateralHoldersParams {
|
|
326
|
-
mint: string
|
|
327
|
-
rangeStart?: number
|
|
328
|
-
rangeEnd?: number
|
|
329
|
-
pdas?: boolean
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
export interface LoopscaleHistoricalCollateralHolder {
|
|
333
|
-
collateralMint: string
|
|
334
|
-
rangeStart: number
|
|
335
|
-
rangeEnd: number
|
|
336
|
-
userDepositSeconds: Record<string, number>
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
export interface LoopscaleVaultDepositorsParams {
|
|
340
|
-
vaultAddresses?: string[]
|
|
341
|
-
principalMints?: string[]
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
export interface LoopscaleVaultDepositor {
|
|
345
|
-
vaultAddress: string
|
|
346
|
-
userDeposits: Array<{ userAddress: string; amountSupplied: string }>
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
export interface LoopscaleVaultInfoParams {
|
|
350
|
-
vaultAddresses?: string[]
|
|
351
|
-
vaultIdentifiers?: string[]
|
|
352
|
-
principalMints?: string[]
|
|
353
|
-
includeRewards?: boolean
|
|
354
|
-
page: number
|
|
355
|
-
pageSize: number
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
export interface LoopscaleVaultInfoResponse {
|
|
359
|
-
lendVaults: Array<{
|
|
360
|
-
vault: { address: string; vaultIdentifier: string; principalMint: string; lpMint: string }
|
|
361
|
-
vaultMetadata: { name: string; description: string; managerName: string; depositCap: number }
|
|
362
|
-
vaultStrategy: { strategy: { address: string; tokenBalance: number; currentDeployedAmount: number; [key: string]: unknown } }
|
|
363
|
-
[key: string]: unknown
|
|
364
|
-
}>
|
|
365
|
-
total: number
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
export interface LoopscaleLoopInfoParams {
|
|
369
|
-
loopVaults?: string[]
|
|
370
|
-
tags?: string[]
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
export interface LoopscaleLoopVaultInfo {
|
|
374
|
-
collateralMint: string
|
|
375
|
-
principalMint: string
|
|
376
|
-
collateralDeposited: number
|
|
377
|
-
collateralApyPct: number
|
|
378
|
-
maxLeverage: number
|
|
379
|
-
name: string
|
|
380
|
-
maxLeveragedApyPct: number
|
|
381
|
-
principalAmountAvailable: number
|
|
382
|
-
[key: string]: unknown
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// ============================================================================
|
|
386
|
-
// Types — Results
|
|
387
|
-
// ============================================================================
|
|
388
|
-
|
|
389
|
-
export interface LoopscaleGeneratedInstruction {
|
|
390
|
-
name: LoopscaleInstructionName | null
|
|
391
|
-
instruction: TransactionInstruction
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
export interface LoopscaleGeneratedInstructionBundle {
|
|
395
|
-
transaction: VersionedTransaction
|
|
396
|
-
instructions: TransactionInstruction[]
|
|
397
|
-
loopscaleInstructions: LoopscaleGeneratedInstruction[]
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
export interface LoopscaleTxResult {
|
|
401
|
-
/** Loopscale instructions wrapped via loopscaleAction.* — ready for createVaultSyncTransaction. */
|
|
402
|
-
syncActions: LoopscaleInstruction[]
|
|
403
|
-
/** Non-Loopscale instructions that belong outside the sync tx (compute budget, etc.). */
|
|
404
|
-
topLevelInstructions: TransactionInstruction[]
|
|
405
|
-
/** Raw decompiled bundle. */
|
|
406
|
-
raw: LoopscaleGeneratedInstructionBundle
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
export interface LoopscaleStrategyBatch {
|
|
410
|
-
/** Loopscale instructions wrapped via loopscaleAction.* — ready for createVaultSyncTransaction. */
|
|
411
|
-
syncActions: LoopscaleInstruction[]
|
|
412
|
-
/** Non-Loopscale instructions that belong outside the sync tx (compute budget, oracles, etc.). */
|
|
413
|
-
topLevelInstructions: TransactionInstruction[]
|
|
414
|
-
/** Setup instructions returned by Loopscale that should not be wrapped in the sync tx. */
|
|
415
|
-
setupInstructions: TransactionInstruction[]
|
|
416
|
-
/** Raw decompiled batch. */
|
|
417
|
-
raw: LoopscaleGeneratedInstructionBundle
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
export interface LoopscaleStrategyBatchesResult {
|
|
421
|
-
batches: LoopscaleStrategyBatch[]
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
export interface LoopscaleStrategyResult extends LoopscaleStrategyBatchesResult {
|
|
425
|
-
strategyAddress: PublicKey
|
|
426
|
-
noncePublicKey: PublicKey
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
export interface LoopscaleUpdateStrategyResult extends LoopscaleStrategyBatchesResult {}
|
|
430
|
-
|
|
431
|
-
export interface LoopscaleCreateLoanResult extends LoopscaleTxResult {
|
|
432
|
-
loanAddress: PublicKey
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
export interface VaultExecutionContext {
|
|
436
|
-
owner: PublicKey
|
|
437
|
-
vaultPda: PublicKey
|
|
438
|
-
signer: PublicKey
|
|
439
|
-
signers: Signer[]
|
|
440
|
-
vaultAddress?: PublicKey
|
|
441
|
-
accountIndex?: number
|
|
442
|
-
policyPda?: PublicKey
|
|
443
|
-
constraintIndices?: number[]
|
|
444
|
-
leadingAccounts?: PublicKey[] | AccountMeta[]
|
|
445
|
-
preHookAccounts?: PublicKey[] | AccountMeta[]
|
|
446
|
-
postHookAccounts?: PublicKey[] | AccountMeta[]
|
|
447
|
-
squadsProgram?: PublicKey
|
|
448
|
-
addressLookupTableAccounts?: import("@solana/web3.js").AddressLookupTableAccount[]
|
|
449
|
-
prependInstructions?: TransactionInstruction[]
|
|
450
|
-
sendOptions?: SendOptions
|
|
451
|
-
commitment?: Commitment
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
export interface BatchExecutionResult {
|
|
455
|
-
batchIndex: number
|
|
456
|
-
signature: string
|
|
457
|
-
logs: string[] | null
|
|
458
|
-
error: unknown
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
export interface ExecutionResult {
|
|
462
|
-
error: unknown
|
|
463
|
-
results: BatchExecutionResult[]
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
export interface ClassifiedInstructions {
|
|
467
|
-
/** Loopscale instructions to wrap in sync tx. */
|
|
468
|
-
loopscale: LoopscaleGeneratedInstruction[]
|
|
469
|
-
/** Compute budget, oracles, etc. for outer tx. */
|
|
470
|
-
topLevel: TransactionInstruction[]
|
|
471
|
-
/** ATA creation etc. — safe to skip. */
|
|
472
|
-
setup: TransactionInstruction[]
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
export interface LoopscaleQuote {
|
|
476
|
-
apy: number
|
|
477
|
-
ltv: number
|
|
478
|
-
liquidationThreshold: number
|
|
479
|
-
maxPrincipalAvailable: number
|
|
480
|
-
strategy?: string
|
|
481
|
-
[key: string]: unknown
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// ============================================================================
|
|
485
|
-
// Discriminator helpers
|
|
486
|
-
// ============================================================================
|
|
487
|
-
|
|
488
|
-
type LoopscaleInstructionName =
|
|
489
|
-
| "createLoan"
|
|
490
|
-
| "depositCollateral"
|
|
491
|
-
| "borrowPrincipal"
|
|
492
|
-
| "repayPrincipal"
|
|
493
|
-
| "withdrawCollateral"
|
|
494
|
-
| "closeLoan"
|
|
495
|
-
| "updateWeightMatrix"
|
|
496
|
-
| "lockLoan"
|
|
497
|
-
| "unlockLoan"
|
|
498
|
-
| "createStrategy"
|
|
499
|
-
| "depositStrategy"
|
|
500
|
-
| "withdrawStrategy"
|
|
501
|
-
| "closeStrategy"
|
|
502
|
-
| "updateStrategy"
|
|
503
|
-
| "refinanceLedger"
|
|
504
|
-
|
|
505
|
-
const DISCRIMINATOR_HEX_TO_NAME = new Map<string, LoopscaleInstructionName>(
|
|
506
|
-
Object.entries(LOOPSCALE_DISCRIMINATORS).map(([name, buf]) => [
|
|
507
|
-
Buffer.from(buf).toString("hex"),
|
|
508
|
-
name as LoopscaleInstructionName,
|
|
509
|
-
]),
|
|
510
|
-
)
|
|
511
|
-
|
|
512
|
-
const NAME_TO_ACTION_WRAPPER: Record<LoopscaleInstructionName, (ix: TransactionInstruction) => LoopscaleInstruction> = {
|
|
513
|
-
createLoan: (ix) => loopscaleAction.createLoan({ instruction: ix }),
|
|
514
|
-
depositCollateral: (ix) => loopscaleAction.depositCollateral({ instruction: ix }),
|
|
515
|
-
borrowPrincipal: (ix) => loopscaleAction.borrowPrincipal({ instruction: ix }),
|
|
516
|
-
repayPrincipal: (ix) => loopscaleAction.repayPrincipal({ instruction: ix }),
|
|
517
|
-
withdrawCollateral: (ix) => loopscaleAction.withdrawCollateral({ instruction: ix }),
|
|
518
|
-
closeLoan: (ix) => loopscaleAction.closeLoan({ instruction: ix }),
|
|
519
|
-
updateWeightMatrix: (ix) => loopscaleAction.updateWeightMatrix({ instruction: ix }),
|
|
520
|
-
createStrategy: (ix) => loopscaleAction.createStrategy({ instruction: ix }),
|
|
521
|
-
depositStrategy: (ix) => loopscaleAction.depositStrategy({ instruction: ix }),
|
|
522
|
-
withdrawStrategy: (ix) => loopscaleAction.withdrawStrategy({ instruction: ix }),
|
|
523
|
-
closeStrategy: (ix) => loopscaleAction.closeStrategy({ instruction: ix }),
|
|
524
|
-
updateStrategy: (ix) => loopscaleAction.updateStrategy({ instruction: ix }),
|
|
525
|
-
lockLoan: (ix) => loopscaleAction.lockLoan({ instruction: ix }),
|
|
526
|
-
unlockLoan: (ix) => loopscaleAction.unlockLoan({ instruction: ix }),
|
|
527
|
-
refinanceLedger: (ix) => loopscaleAction.refinanceLedger({ instruction: ix }),
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
interface LoopscaleLegacyTransactionEntry {
|
|
531
|
-
message: string
|
|
532
|
-
signatures: Array<{ publicKey: string; signature: string }>
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// ============================================================================
|
|
536
|
-
// Internal helpers
|
|
537
|
-
// ============================================================================
|
|
538
|
-
|
|
539
|
-
function toBase58(value: PublicKey | string): string {
|
|
540
|
-
return typeof value === "string" ? value : value.toBase58()
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
function toSafeNumber(value: bigint | number, field: string): number {
|
|
544
|
-
if (typeof value === "bigint") {
|
|
545
|
-
if (value < 0n) throw new Error(`${field} must be non-negative`)
|
|
546
|
-
if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`${field} too large for JSON integer`)
|
|
547
|
-
return Number(value)
|
|
548
|
-
}
|
|
549
|
-
if (!Number.isInteger(value) || value < 0) throw new Error(`${field} must be a non-negative integer`)
|
|
550
|
-
return value
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
function isEmptySignature(sig: Uint8Array): boolean {
|
|
554
|
-
return sig.every((b) => b === 0)
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
function identifyInstruction(ix: TransactionInstruction): LoopscaleInstructionName | null {
|
|
558
|
-
if (!ix.programId.equals(LOOPSCALE_PROGRAM_ID)) return null
|
|
559
|
-
const hex = Buffer.from(ix.data).subarray(0, 8).toString("hex")
|
|
560
|
-
return DISCRIMINATOR_HEX_TO_NAME.get(hex) ?? null
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
async function decompileInstructions(
|
|
564
|
-
connection: Connection,
|
|
565
|
-
transaction: VersionedTransaction,
|
|
566
|
-
): Promise<TransactionInstruction[]> {
|
|
567
|
-
const lookupTableAccounts = await Promise.all(
|
|
568
|
-
transaction.message.addressTableLookups.map(async (lookup) => {
|
|
569
|
-
const result = await connection.getAddressLookupTable(lookup.accountKey)
|
|
570
|
-
if (!result.value) {
|
|
571
|
-
throw new Error(`Missing lookup table ${lookup.accountKey.toBase58()}`)
|
|
572
|
-
}
|
|
573
|
-
return result.value
|
|
574
|
-
}),
|
|
575
|
-
)
|
|
576
|
-
return TransactionMessage.decompile(transaction.message, { addressLookupTableAccounts: lookupTableAccounts }).instructions
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
function buildTxFromLegacyEntry(entry: {
|
|
580
|
-
message: string
|
|
581
|
-
signatures: Array<{ publicKey: string; signature: string }>
|
|
582
|
-
}): VersionedTransaction {
|
|
583
|
-
const message = VersionedMessage.deserialize(Buffer.from(entry.message, "base64"))
|
|
584
|
-
const tx = new VersionedTransaction(message)
|
|
585
|
-
const signerKeys = message.staticAccountKeys.slice(0, message.header.numRequiredSignatures)
|
|
586
|
-
|
|
587
|
-
for (const sig of entry.signatures) {
|
|
588
|
-
const pubkey = new PublicKey(sig.publicKey)
|
|
589
|
-
const sigBytes = Buffer.from(sig.signature, "base64")
|
|
590
|
-
if (sigBytes.length !== 64) throw new Error(`Invalid signature for ${pubkey.toBase58()}`)
|
|
591
|
-
const idx = signerKeys.findIndex((k) => k.equals(pubkey))
|
|
592
|
-
if (idx >= 0) tx.signatures[idx] = new Uint8Array(sigBytes)
|
|
593
|
-
}
|
|
594
|
-
return tx
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
function mergeInputSignatures(
|
|
598
|
-
resultTx: VersionedTransaction,
|
|
599
|
-
inputTx: VersionedTransaction,
|
|
600
|
-
): VersionedTransaction {
|
|
601
|
-
const signerKeys = resultTx.message.staticAccountKeys.slice(0, resultTx.message.header.numRequiredSignatures)
|
|
602
|
-
const inputKeys = inputTx.message.staticAccountKeys
|
|
603
|
-
const inputNumSigners = inputTx.message.header.numRequiredSignatures
|
|
604
|
-
|
|
605
|
-
for (let i = 0; i < inputNumSigners; i++) {
|
|
606
|
-
const inputSig = inputTx.signatures[i]
|
|
607
|
-
if (isEmptySignature(inputSig)) continue
|
|
608
|
-
const resultIdx = signerKeys.findIndex((key) => key.equals(inputKeys[i]))
|
|
609
|
-
if (resultIdx < 0) continue
|
|
610
|
-
if (isEmptySignature(resultTx.signatures[resultIdx])) {
|
|
611
|
-
resultTx.signatures[resultIdx] = inputSig
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
return resultTx
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
function ensureMessageMatches(a: VersionedTransaction, b: VersionedTransaction): void {
|
|
618
|
-
const am = Buffer.from(a.message.serialize())
|
|
619
|
-
const bm = Buffer.from(b.message.serialize())
|
|
620
|
-
if (!am.equals(bm)) {
|
|
621
|
-
throw new Error("Loopscale returned a different transaction message; refusing to merge")
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// ============================================================================
|
|
626
|
-
// LoopscaleClient
|
|
627
|
-
// ============================================================================
|
|
628
|
-
|
|
629
|
-
export class LoopscaleClient {
|
|
630
|
-
private readonly connection: Connection
|
|
631
|
-
private readonly baseUrl: string
|
|
632
|
-
private readonly userWallet: string
|
|
633
|
-
private readonly bsAuth: PublicKey
|
|
634
|
-
private readonly useMpcCoSign: boolean
|
|
635
|
-
private readonly debug: boolean
|
|
636
|
-
private vaultCtx: VaultExecutionContext | null = null
|
|
637
|
-
|
|
638
|
-
constructor(config: LoopscaleClientConfig) {
|
|
639
|
-
this.connection = config.connection
|
|
640
|
-
this.baseUrl = config.baseUrl ?? LOOPSCALE_API_BASE_URL
|
|
641
|
-
this.userWallet = toBase58(config.userWallet)
|
|
642
|
-
this.bsAuth = config.bsAuth ?? BS_AUTH
|
|
643
|
-
this.useMpcCoSign = config.useMpcCoSign ?? true
|
|
644
|
-
this.debug = config.debug ?? false
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// ── Vault execution context ──
|
|
648
|
-
|
|
649
|
-
setVaultContext(ctx: VaultExecutionContext): void {
|
|
650
|
-
this.vaultCtx = ctx
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
private requireVaultContext(): VaultExecutionContext {
|
|
654
|
-
if (!this.vaultCtx) {
|
|
655
|
-
throw new Error("LoopscaleClient: call setVaultContext() before executeBatches/executeSyncTx")
|
|
656
|
-
}
|
|
657
|
-
return this.vaultCtx
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
async executeBatches(batches: LoopscaleStrategyBatch[]): Promise<ExecutionResult> {
|
|
661
|
-
const ctx = this.requireVaultContext()
|
|
662
|
-
const commitment = ctx.commitment ?? "confirmed"
|
|
663
|
-
const results: BatchExecutionResult[] = []
|
|
664
|
-
|
|
665
|
-
for (const [batchIndex, batch] of batches.entries()) {
|
|
666
|
-
const result = batch.syncActions.length === 0
|
|
667
|
-
? await this.executePrebuiltBatch(batch, batchIndex, commitment)
|
|
668
|
-
: await this.executeSyncBatch(batch, batchIndex, ctx, commitment)
|
|
669
|
-
|
|
670
|
-
results.push(result)
|
|
671
|
-
if (result.error) {
|
|
672
|
-
return { error: result.error, results }
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
return { error: null, results }
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
async executeSyncTx(syncActions: LoopscaleInstruction[]): Promise<BatchExecutionResult> {
|
|
680
|
-
const ctx = this.requireVaultContext()
|
|
681
|
-
const commitment = ctx.commitment ?? "confirmed"
|
|
682
|
-
return this.executeSyncBatch(
|
|
683
|
-
{ syncActions, topLevelInstructions: [], setupInstructions: [] },
|
|
684
|
-
0, ctx, commitment,
|
|
685
|
-
)
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
private async executePrebuiltBatch(
|
|
689
|
-
batch: LoopscaleStrategyBatch,
|
|
690
|
-
batchIndex: number,
|
|
691
|
-
commitment: Commitment,
|
|
692
|
-
): Promise<BatchExecutionResult> {
|
|
693
|
-
const ctx = this.requireVaultContext()
|
|
694
|
-
const tx = batch.raw.transaction
|
|
695
|
-
const signerKeys = tx.message.staticAccountKeys.slice(0, tx.message.header.numRequiredSignatures)
|
|
696
|
-
const available = new Map(ctx.signers.map((s) => [s.publicKey.toBase58(), s]))
|
|
697
|
-
const local = signerKeys.map((k) => available.get(k.toBase58())).filter((s): s is Signer => Boolean(s))
|
|
698
|
-
|
|
699
|
-
if (local.length > 0) tx.sign(local)
|
|
700
|
-
|
|
701
|
-
const missing = signerKeys.filter((_, i) => tx.signatures[i]?.every((b) => b === 0))
|
|
702
|
-
if (missing.length > 0) {
|
|
703
|
-
throw new Error(`batch ${batchIndex + 1}: missing signer(s): ${missing.map((k) => k.toBase58()).join(", ")}`)
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
return { batchIndex, ...(await this.sendAndConfirm(tx, ctx.sendOptions, { commitment })) }
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
private async executeSyncBatch(
|
|
710
|
-
batch: Pick<LoopscaleStrategyBatch, "syncActions" | "topLevelInstructions" | "setupInstructions">,
|
|
711
|
-
batchIndex: number,
|
|
712
|
-
ctx: VaultExecutionContext,
|
|
713
|
-
commitment: Commitment,
|
|
714
|
-
): Promise<BatchExecutionResult> {
|
|
715
|
-
const syncResult = await createVaultSyncTransaction({
|
|
716
|
-
instructions: batch.syncActions,
|
|
717
|
-
owner: ctx.owner,
|
|
718
|
-
connection: this.connection,
|
|
719
|
-
policyPda: ctx.policyPda,
|
|
720
|
-
vaultPda: ctx.vaultPda,
|
|
721
|
-
signer: ctx.signer,
|
|
722
|
-
accountIndex: ctx.accountIndex,
|
|
723
|
-
constraintIndices: ctx.constraintIndices,
|
|
724
|
-
vaultAddress: ctx.vaultAddress,
|
|
725
|
-
leadingAccounts: ctx.leadingAccounts,
|
|
726
|
-
preHookAccounts: ctx.preHookAccounts,
|
|
727
|
-
postHookAccounts: ctx.postHookAccounts,
|
|
728
|
-
squadsProgram: ctx.squadsProgram,
|
|
729
|
-
})
|
|
730
|
-
|
|
731
|
-
if (syncResult.setupInstructions.length > 0) {
|
|
732
|
-
throw new Error(`batch ${batchIndex + 1}: unexpected sync setupInstructions (${syncResult.setupInstructions.length})`)
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
const { blockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash(commitment)
|
|
736
|
-
const msg = new TransactionMessage({
|
|
737
|
-
payerKey: ctx.signers[0]!.publicKey,
|
|
738
|
-
recentBlockhash: blockhash,
|
|
739
|
-
instructions: [
|
|
740
|
-
...(ctx.prependInstructions ?? []),
|
|
741
|
-
...batch.topLevelInstructions,
|
|
742
|
-
...syncResult.preInstructions,
|
|
743
|
-
syncResult.instruction,
|
|
744
|
-
...syncResult.postInstructions,
|
|
745
|
-
],
|
|
746
|
-
}).compileToV0Message(ctx.addressLookupTableAccounts ?? [])
|
|
747
|
-
|
|
748
|
-
let tx = new VersionedTransaction(msg)
|
|
749
|
-
tx.sign([...ctx.signers, ...syncResult.signers])
|
|
750
|
-
tx = await this.coSign(tx)
|
|
751
|
-
|
|
752
|
-
return {
|
|
753
|
-
batchIndex,
|
|
754
|
-
...(await this.sendAndConfirm(tx, ctx.sendOptions, { commitment, blockhash, lastValidBlockHeight })),
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
private async sendAndConfirm(
|
|
759
|
-
tx: VersionedTransaction,
|
|
760
|
-
sendOptions: SendOptions | undefined,
|
|
761
|
-
confirmation: { commitment: Commitment; blockhash?: string; lastValidBlockHeight?: number },
|
|
762
|
-
): Promise<{ signature: string; logs: string[] | null; error: unknown }> {
|
|
763
|
-
const signature = await this.connection.sendTransaction(tx, sendOptions)
|
|
764
|
-
|
|
765
|
-
if (confirmation.blockhash && confirmation.lastValidBlockHeight !== undefined) {
|
|
766
|
-
try {
|
|
767
|
-
await this.connection.confirmTransaction({
|
|
768
|
-
signature,
|
|
769
|
-
blockhash: confirmation.blockhash,
|
|
770
|
-
lastValidBlockHeight: confirmation.lastValidBlockHeight,
|
|
771
|
-
}, confirmation.commitment)
|
|
772
|
-
} catch (err) {
|
|
773
|
-
if (this.debug) console.warn(`[LoopscaleClient] confirmTransaction failed for ${signature}:`, err)
|
|
774
|
-
}
|
|
775
|
-
} else {
|
|
776
|
-
for (let attempt = 0; attempt < 60; attempt += 1) {
|
|
777
|
-
const statuses = await this.connection.getSignatureStatuses([signature])
|
|
778
|
-
const s = statuses.value[0]
|
|
779
|
-
if (s?.err || s?.confirmationStatus === "confirmed" || s?.confirmationStatus === "finalized") break
|
|
780
|
-
await new Promise((r) => setTimeout(r, 1_000))
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
const finality: Finality = confirmation.commitment === "finalized" ? "finalized" : "confirmed"
|
|
785
|
-
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
786
|
-
const txResult = await this.connection.getTransaction(signature, {
|
|
787
|
-
commitment: finality,
|
|
788
|
-
maxSupportedTransactionVersion: 0,
|
|
789
|
-
})
|
|
790
|
-
if (txResult) {
|
|
791
|
-
return { signature, logs: txResult.meta?.logMessages ?? null, error: txResult.meta?.err ?? null }
|
|
792
|
-
}
|
|
793
|
-
await new Promise((r) => setTimeout(r, 500))
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
return { signature, logs: null, error: `Transaction ${signature} not found after confirmation timeout` }
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
// ── Data ──
|
|
800
|
-
|
|
801
|
-
async getQuotes(params: LoopscaleQuoteParams): Promise<LoopscaleQuote[]> {
|
|
802
|
-
const body = {
|
|
803
|
-
durationType: params.durationType,
|
|
804
|
-
duration: params.duration,
|
|
805
|
-
principal: toBase58(params.principal),
|
|
806
|
-
collateral: params.collateral.map(toBase58),
|
|
807
|
-
limit: params.limit ?? 10,
|
|
808
|
-
offset: params.offset ?? 0,
|
|
809
|
-
}
|
|
810
|
-
return this.post<LoopscaleQuote[]>("/markets/quote", body, { "user-wallet": this.userWallet })
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
async getMaxQuote(params: LoopscaleMaxQuoteParams): Promise<LoopscaleMaxQuote[]> {
|
|
814
|
-
const body = {
|
|
815
|
-
durationType: params.durationType,
|
|
816
|
-
duration: params.duration,
|
|
817
|
-
principalMint: toBase58(params.principalMint),
|
|
818
|
-
collateralFilter: params.collateralFilter.map((f) => ({
|
|
819
|
-
amount: toSafeNumber(f.amount, "amount"),
|
|
820
|
-
assetData: f.assetData,
|
|
821
|
-
})),
|
|
822
|
-
}
|
|
823
|
-
return this.post<LoopscaleMaxQuote[]>("/markets/quote/max", body, { "user-wallet": this.userWallet })
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
async getLoanInfo(params: LoopscaleLoanInfoParams): Promise<LoopscaleLoanInfoResponse> {
|
|
827
|
-
return this.post<LoopscaleLoanInfoResponse>("/markets/loans/info", params, { "user-wallet": this.userWallet })
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
async getStrategies(params: LoopscaleGetStrategiesParams): Promise<LoopscaleStrategyInfoResponse> {
|
|
831
|
-
return this.post<LoopscaleStrategyInfoResponse>("/markets/strategy/infos", params, { "user-wallet": this.userWallet })
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
async getCollateralHolders(params: LoopscaleCollateralHoldersParams): Promise<LoopscaleCollateralHolder[]> {
|
|
835
|
-
return this.post<LoopscaleCollateralHolder[]>("/markets/collateral/holders", params)
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
async getHistoricalCollateralHolders(params: LoopscaleHistoricalCollateralHoldersParams): Promise<LoopscaleHistoricalCollateralHolder[]> {
|
|
839
|
-
return this.post<LoopscaleHistoricalCollateralHolder[]>("/markets/collateral/holders/historical", params)
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
async getVaultDepositors(params: LoopscaleVaultDepositorsParams): Promise<LoopscaleVaultDepositor[]> {
|
|
843
|
-
return this.post<LoopscaleVaultDepositor[]>("/markets/lending_vaults/deposits", params)
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
async getVaultInfo(params: LoopscaleVaultInfoParams): Promise<LoopscaleVaultInfoResponse> {
|
|
847
|
-
return this.post<LoopscaleVaultInfoResponse>("/markets/lending_vaults/info", params)
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
async getLoopInfo(params: LoopscaleLoopInfoParams): Promise<Record<string, LoopscaleLoopVaultInfo>> {
|
|
851
|
-
return this.post<Record<string, LoopscaleLoopVaultInfo>>("/markets/loop/info", params)
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
// ── Strategy (LENDER side) ──
|
|
855
|
-
|
|
856
|
-
async createStrategy(params: LoopscaleCreateStrategyParams): Promise<LoopscaleStrategyResult> {
|
|
857
|
-
const lender = toBase58(params.lender)
|
|
858
|
-
const body: Record<string, unknown> = {
|
|
859
|
-
principalMint: toBase58(params.principalMint),
|
|
860
|
-
lender,
|
|
861
|
-
amount: toSafeNumber(params.amount, "amount"),
|
|
862
|
-
}
|
|
863
|
-
if (params.originationsEnabled !== undefined) body.originationsEnabled = params.originationsEnabled
|
|
864
|
-
if (params.liquidityBuffer !== undefined) body.liquidityBuffer = params.liquidityBuffer
|
|
865
|
-
if (params.interestFee !== undefined) body.interestFee = params.interestFee
|
|
866
|
-
if (params.originationFee !== undefined) body.originationFee = params.originationFee
|
|
867
|
-
if (params.originationCap !== undefined) body.originationCap = params.originationCap
|
|
868
|
-
if (params.collateralTerms) body.collateralTerms = params.collateralTerms
|
|
869
|
-
if (params.marketInformation) body.marketInformation = toBase58(params.marketInformation)
|
|
870
|
-
if (params.externalYieldSourceArgs) body.externalYieldSourceArgs = params.externalYieldSourceArgs
|
|
871
|
-
|
|
872
|
-
const entries = await this.post<LoopscaleLegacyTransactionEntry[]>(
|
|
873
|
-
"/markets/strategy/create", body, { payer: this.userWallet },
|
|
874
|
-
)
|
|
875
|
-
|
|
876
|
-
if (!Array.isArray(entries) || entries.length === 0) {
|
|
877
|
-
throw new Error("Loopscale strategy/create returned no transactions")
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
const batches = await this.parseStrategyBatches(entries)
|
|
881
|
-
const createBatch = batches.find((batch) => batch.raw.loopscaleInstructions.some((g) => g.name === "createStrategy"))
|
|
882
|
-
const createIx = createBatch?.raw.loopscaleInstructions.find((g) => g.name === "createStrategy")
|
|
883
|
-
if (!createIx) throw new Error("Loopscale strategy/create did not return a createStrategy instruction")
|
|
884
|
-
const strategyAddress = createIx.instruction.keys[3]?.pubkey
|
|
885
|
-
const noncePublicKey = createIx.instruction.keys[2]?.pubkey
|
|
886
|
-
if (!strategyAddress || !noncePublicKey) {
|
|
887
|
-
throw new Error("Invalid createStrategy account layout from API")
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
if (this.debug) {
|
|
891
|
-
console.log(`[LoopscaleClient] createStrategy: strategy=${strategyAddress.toBase58()}, batches=${batches.length}`)
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
return { batches, strategyAddress, noncePublicKey }
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
async depositStrategy(params: LoopscaleDepositStrategyParams): Promise<LoopscaleTxResult> {
|
|
898
|
-
const body = {
|
|
899
|
-
strategy: toBase58(params.strategy),
|
|
900
|
-
amount: toSafeNumber(params.amount, "amount"),
|
|
901
|
-
}
|
|
902
|
-
return this.fetchAndClassifySingleTx("/markets/strategy/deposit", body, {
|
|
903
|
-
"User-Wallet": this.userWallet,
|
|
904
|
-
payer: this.userWallet,
|
|
905
|
-
})
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
async withdrawStrategy(params: LoopscaleWithdrawStrategyParams): Promise<LoopscaleTxResult> {
|
|
909
|
-
const body = {
|
|
910
|
-
strategy: toBase58(params.strategy),
|
|
911
|
-
amount: toSafeNumber(params.amount, "amount"),
|
|
912
|
-
withdrawAll: params.withdrawAll,
|
|
913
|
-
}
|
|
914
|
-
return this.fetchAndClassifySingleTx("/markets/strategy/withdraw", body, {
|
|
915
|
-
"User-Wallet": this.userWallet,
|
|
916
|
-
payer: this.userWallet,
|
|
917
|
-
})
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
async closeStrategy(params: LoopscaleCloseStrategyParams): Promise<LoopscaleTxResult> {
|
|
921
|
-
const strategy = toBase58(params.strategy)
|
|
922
|
-
return this.fetchAndClassifySingleTx(`/markets/strategy/close/${strategy}`, undefined, {
|
|
923
|
-
payer: this.userWallet,
|
|
924
|
-
})
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
async updateStrategy(params: LoopscaleUpdateStrategyParams): Promise<LoopscaleUpdateStrategyResult> {
|
|
928
|
-
const body: Record<string, unknown> = { strategy: toBase58(params.strategy) }
|
|
929
|
-
if (params.collateralTerms) body.collateralTerms = params.collateralTerms
|
|
930
|
-
if (params.updateParams) body.updateParams = params.updateParams
|
|
931
|
-
|
|
932
|
-
const entries = await this.post<LoopscaleLegacyTransactionEntry[]>(
|
|
933
|
-
"/markets/strategy/update", body, { "User-Wallet": this.userWallet, payer: this.userWallet },
|
|
934
|
-
)
|
|
935
|
-
|
|
936
|
-
if (!Array.isArray(entries) || entries.length === 0) {
|
|
937
|
-
throw new Error("Loopscale strategy/update returned no transactions")
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
const batches = await this.parseStrategyBatches(entries)
|
|
941
|
-
|
|
942
|
-
if (this.debug) {
|
|
943
|
-
console.log(`[LoopscaleClient] updateStrategy: batches=${batches.length}`)
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
return { batches }
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
// ── Loan (BORROWER side) ──
|
|
950
|
-
|
|
951
|
-
/**
|
|
952
|
-
* Build a standalone borrow_principal instruction (no lock/unlock needed).
|
|
953
|
-
* Requires collateral to already be deposited in the loan.
|
|
954
|
-
*/
|
|
955
|
-
buildBorrowPrincipalInstruction(params: LoopscaleBorrowPrincipalParams): TransactionInstruction {
|
|
956
|
-
const borrower = new PublicKey(toBase58(params.borrower))
|
|
957
|
-
const loan = new PublicKey(toBase58(params.loan))
|
|
958
|
-
const strategy = new PublicKey(toBase58(params.strategy))
|
|
959
|
-
const marketInformation = new PublicKey(toBase58(params.marketInformation))
|
|
960
|
-
const principalMint = new PublicKey(toBase58(params.principalMint))
|
|
961
|
-
const amount = typeof params.amount === "bigint" ? params.amount : BigInt(params.amount)
|
|
962
|
-
|
|
963
|
-
const guidance = params.assetIndexGuidance
|
|
964
|
-
const duration = params.durationIndex
|
|
965
|
-
const expectedApy = params.expectedApy !== undefined
|
|
966
|
-
? (typeof params.expectedApy === "bigint" ? params.expectedApy : BigInt(params.expectedApy))
|
|
967
|
-
: 0n
|
|
968
|
-
const expectedLqt = params.expectedLqt ?? [0, 0, 0, 0, 0]
|
|
969
|
-
const skipSolUnwrap = params.skipSolUnwrap ?? false
|
|
970
|
-
|
|
971
|
-
// Serialize instruction data: discriminator + amount + guidance_vec + duration + expected_loan_values + skip_sol_unwrap
|
|
972
|
-
const dataSize = 8 + 8 + 4 + guidance.length + 1 + 8 + 20 + 1
|
|
973
|
-
const data = Buffer.alloc(dataSize)
|
|
974
|
-
let offset = 0
|
|
975
|
-
Buffer.from(LOOPSCALE_DISCRIMINATORS.borrowPrincipal).copy(data, 0); offset += 8
|
|
976
|
-
data.writeBigUInt64LE(amount, offset); offset += 8
|
|
977
|
-
data.writeUInt32LE(guidance.length, offset); offset += 4
|
|
978
|
-
for (const b of guidance) { data.writeUInt8(b, offset); offset += 1 }
|
|
979
|
-
data.writeUInt8(duration, offset); offset += 1
|
|
980
|
-
data.writeBigUInt64LE(expectedApy, offset); offset += 8
|
|
981
|
-
for (let i = 0; i < 5; i++) { data.writeUInt32LE(expectedLqt[i] ?? 0, offset); offset += 4 }
|
|
982
|
-
data.writeUInt8(skipSolUnwrap ? 1 : 0, offset); offset += 1
|
|
983
|
-
|
|
984
|
-
const TOKEN_PROGRAM_ID = new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
|
985
|
-
const [borrowerTa] = PublicKey.findProgramAddressSync(
|
|
986
|
-
[borrower.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), principalMint.toBuffer()],
|
|
987
|
-
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
988
|
-
)
|
|
989
|
-
const [strategyTa] = PublicKey.findProgramAddressSync(
|
|
990
|
-
[strategy.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), principalMint.toBuffer()],
|
|
991
|
-
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
992
|
-
)
|
|
993
|
-
const eventAuthority = PublicKey.findProgramAddressSync(
|
|
994
|
-
[Buffer.from("__event_authority")],
|
|
995
|
-
LOOPSCALE_PROGRAM_ID,
|
|
996
|
-
)[0]
|
|
997
|
-
|
|
998
|
-
const ix = new TransactionInstruction({
|
|
999
|
-
programId: LOOPSCALE_PROGRAM_ID,
|
|
1000
|
-
keys: [
|
|
1001
|
-
{ pubkey: this.bsAuth, isSigner: true, isWritable: false },
|
|
1002
|
-
{ pubkey: borrower, isSigner: true, isWritable: true },
|
|
1003
|
-
{ pubkey: borrower, isSigner: true, isWritable: false },
|
|
1004
|
-
{ pubkey: loan, isSigner: false, isWritable: true },
|
|
1005
|
-
{ pubkey: strategy, isSigner: false, isWritable: true },
|
|
1006
|
-
{ pubkey: marketInformation, isSigner: false, isWritable: true },
|
|
1007
|
-
{ pubkey: principalMint, isSigner: false, isWritable: false },
|
|
1008
|
-
{ pubkey: borrowerTa, isSigner: false, isWritable: true },
|
|
1009
|
-
{ pubkey: strategyTa, isSigner: false, isWritable: true },
|
|
1010
|
-
{ pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
|
1011
|
-
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
|
1012
|
-
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
1013
|
-
{ pubkey: eventAuthority, isSigner: false, isWritable: false },
|
|
1014
|
-
{ pubkey: LOOPSCALE_PROGRAM_ID, isSigner: false, isWritable: false },
|
|
1015
|
-
...params.healthCheckAccounts,
|
|
1016
|
-
],
|
|
1017
|
-
data,
|
|
1018
|
-
})
|
|
1019
|
-
|
|
1020
|
-
return ix
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
/**
|
|
1024
|
-
* Build a standalone borrow_principal wrapped as a LoopscaleInstruction for sync tx.
|
|
1025
|
-
* Requires collateral to already be deposited in the loan.
|
|
1026
|
-
*/
|
|
1027
|
-
buildBorrowPrincipalAction(params: LoopscaleBorrowPrincipalParams): LoopscaleInstruction {
|
|
1028
|
-
return loopscaleAction.borrowPrincipal({ instruction: this.buildBorrowPrincipalInstruction(params) })
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
async repayLoan(params: LoopscaleRepayLoanParams): Promise<LoopscaleTxResult> {
|
|
1032
|
-
const body: Record<string, unknown> = {
|
|
1033
|
-
loan: toBase58(params.loan),
|
|
1034
|
-
repayParams: params.repayParams.map((p) => ({
|
|
1035
|
-
amount: toSafeNumber(p.amount, "repayAmount"),
|
|
1036
|
-
ledgerIndex: p.ledgerIndex,
|
|
1037
|
-
repayAll: p.repayAll,
|
|
1038
|
-
})),
|
|
1039
|
-
collateralWithdrawalParams: (params.collateralWithdrawalParams ?? []).map((p) => ({
|
|
1040
|
-
amount: toSafeNumber(p.amount, "withdrawAmount"),
|
|
1041
|
-
collateralMint: toBase58(p.collateralMint),
|
|
1042
|
-
})),
|
|
1043
|
-
}
|
|
1044
|
-
if (params.closeIfPossible !== undefined) body.closeIfPossible = params.closeIfPossible
|
|
1045
|
-
|
|
1046
|
-
return this.fetchAndClassifyWrappedTx("/markets/creditbook/repay", body, { "user-wallet": this.userWallet })
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
async closeLoan(params: LoopscaleCloseLoanParams): Promise<LoopscaleTxResult> {
|
|
1050
|
-
const body = { loan: toBase58(params.loan) }
|
|
1051
|
-
return this.fetchAndClassifyWrappedTx("/markets/creditbook/close_loan", body, { "user-wallet": this.userWallet })
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
async createLoan(params: LoopscaleCreateLoanParams): Promise<LoopscaleCreateLoanResult> {
|
|
1055
|
-
const body: Record<string, unknown> = {
|
|
1056
|
-
borrower: toBase58(params.borrower),
|
|
1057
|
-
depositCollateral: params.depositCollateral.map((c) => ({
|
|
1058
|
-
collateralAmount: toSafeNumber(c.collateralAmount, "collateralAmount"),
|
|
1059
|
-
collateralAssetData: c.collateralAssetData,
|
|
1060
|
-
})),
|
|
1061
|
-
principalRequested: params.principalRequested.map((p) => ({
|
|
1062
|
-
ledgerIndex: p.ledgerIndex,
|
|
1063
|
-
principalAmount: toSafeNumber(p.principalAmount, "principalAmount"),
|
|
1064
|
-
principalMint: toBase58(p.principalMint),
|
|
1065
|
-
strategy: toBase58(p.strategy),
|
|
1066
|
-
durationIndex: p.durationIndex,
|
|
1067
|
-
expectedLoanValues: p.expectedLoanValues ?? { expectedApy: 0, expectedLqt: [0, 0, 0, 0, 0] },
|
|
1068
|
-
})),
|
|
1069
|
-
}
|
|
1070
|
-
if (params.assetIndexGuidance) body.assetIndexGuidance = params.assetIndexGuidance
|
|
1071
|
-
if (params.loanNonce) body.loanNonce = params.loanNonce
|
|
1072
|
-
if (params.isLoop !== undefined) body.isLoop = params.isLoop
|
|
1073
|
-
|
|
1074
|
-
const headers = { payer: this.userWallet }
|
|
1075
|
-
|
|
1076
|
-
const data = await this.post<{
|
|
1077
|
-
transaction: { message: string; signatures: Array<{ publicKey: string; signature: string }> }
|
|
1078
|
-
loanAddress: string
|
|
1079
|
-
}>("/markets/creditbook/create", body, headers)
|
|
1080
|
-
|
|
1081
|
-
const tx = buildTxFromLegacyEntry(data.transaction)
|
|
1082
|
-
const instructions = await decompileInstructions(this.connection, tx)
|
|
1083
|
-
const classified = this.classifyInstructions(instructions)
|
|
1084
|
-
const syncActions = this.buildSyncActions(classified.loopscale)
|
|
1085
|
-
const raw = this.buildRawBundle(tx, instructions, classified.loopscale)
|
|
1086
|
-
const loanAddress = new PublicKey(data.loanAddress)
|
|
1087
|
-
|
|
1088
|
-
if (this.debug) {
|
|
1089
|
-
console.log(`[LoopscaleClient] createLoan: loan=${loanAddress.toBase58()}, syncActions=${syncActions.length}`)
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
return { syncActions, topLevelInstructions: classified.topLevel, raw, loanAddress }
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
async apiBorrowPrincipal(params: LoopscaleApiBorrowPrincipalParams): Promise<LoopscaleTxResult> {
|
|
1096
|
-
const borrowParams: Record<string, unknown> = {
|
|
1097
|
-
amount: toSafeNumber(params.borrowParams.amount, "amount"),
|
|
1098
|
-
duration: params.borrowParams.durationIndex,
|
|
1099
|
-
}
|
|
1100
|
-
if (params.borrowParams.expectedLoanValues) {
|
|
1101
|
-
borrowParams.expectedLoanValues = params.borrowParams.expectedLoanValues
|
|
1102
|
-
}
|
|
1103
|
-
const body: Record<string, unknown> = {
|
|
1104
|
-
loan: toBase58(params.loan),
|
|
1105
|
-
borrowParams,
|
|
1106
|
-
strategy: toBase58(params.strategy),
|
|
1107
|
-
}
|
|
1108
|
-
if (params.refinanceParams) body.refinanceParams = params.refinanceParams
|
|
1109
|
-
if (params.isLoop !== undefined) body.isLoop = params.isLoop
|
|
1110
|
-
|
|
1111
|
-
return this.fetchAndClassifySingleTx("/markets/creditbook/borrow", body, { payer: this.userWallet })
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
async depositCollateral(params: LoopscaleDepositCollateralParams): Promise<LoopscaleTxResult> {
|
|
1115
|
-
const body: Record<string, unknown> = {
|
|
1116
|
-
loan: toBase58(params.loan),
|
|
1117
|
-
depositMint: toBase58(params.depositMint),
|
|
1118
|
-
amount: toSafeNumber(params.amount, "amount"),
|
|
1119
|
-
assetType: params.assetType,
|
|
1120
|
-
assetIdentifier: toBase58(params.assetIdentifier),
|
|
1121
|
-
}
|
|
1122
|
-
if (params.assetIndexGuidance) body.assetIndexGuidance = params.assetIndexGuidance
|
|
1123
|
-
if (params.expectedLoanValues) body.expectedLoanValues = params.expectedLoanValues
|
|
1124
|
-
|
|
1125
|
-
return this.fetchAndClassifySingleTx("/markets/creditbook/collateral/deposit", body, {
|
|
1126
|
-
"user-wallet": this.userWallet,
|
|
1127
|
-
payer: this.userWallet,
|
|
1128
|
-
})
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
async withdrawCollateral(params: LoopscaleWithdrawCollateralParams): Promise<LoopscaleTxResult> {
|
|
1132
|
-
const body: Record<string, unknown> = {
|
|
1133
|
-
loan: toBase58(params.loan),
|
|
1134
|
-
collateralMint: toBase58(params.collateralMint),
|
|
1135
|
-
amount: toSafeNumber(params.amount, "amount"),
|
|
1136
|
-
collateralIndex: params.collateralIndex,
|
|
1137
|
-
}
|
|
1138
|
-
if (params.expectedLoanValues) body.expectedLoanValues = params.expectedLoanValues
|
|
1139
|
-
if (params.assetIndexGuidance) body.assetIndexGuidance = params.assetIndexGuidance
|
|
1140
|
-
|
|
1141
|
-
return this.fetchAndClassifySingleTx("/markets/creditbook/collateral/withdraw", body, {
|
|
1142
|
-
"user-wallet": this.userWallet,
|
|
1143
|
-
payer: this.userWallet,
|
|
1144
|
-
})
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
async refinanceLoan(params: LoopscaleRefinanceLoanParams): Promise<LoopscaleTxResult> {
|
|
1148
|
-
const body = {
|
|
1149
|
-
loan: toBase58(params.loan),
|
|
1150
|
-
oldStrategy: toBase58(params.oldStrategy),
|
|
1151
|
-
newStrategy: toBase58(params.newStrategy),
|
|
1152
|
-
refinanceParams: params.refinanceParams,
|
|
1153
|
-
}
|
|
1154
|
-
return this.fetchAndClassifyWrappedTx("/markets/creditbook/refinance", body, {
|
|
1155
|
-
"user-wallet": this.userWallet,
|
|
1156
|
-
})
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
// ── Co-signing ──
|
|
1160
|
-
|
|
1161
|
-
async coSign(transaction: VersionedTransaction): Promise<VersionedTransaction> {
|
|
1162
|
-
const serializedTx = Buffer.from(transaction.serialize()).toString("base64")
|
|
1163
|
-
const serializedMsg = Buffer.from(transaction.message.serialize()).toString("base64")
|
|
1164
|
-
|
|
1165
|
-
if (this.useMpcCoSign) {
|
|
1166
|
-
const body = {
|
|
1167
|
-
batches: [{
|
|
1168
|
-
transactions: [{
|
|
1169
|
-
identifier: "loopscale-cosign",
|
|
1170
|
-
transaction: serializedTx,
|
|
1171
|
-
transactionType: 1,
|
|
1172
|
-
}],
|
|
1173
|
-
}],
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
const data = await this.post<{
|
|
1177
|
-
batches: Array<{ transactions: Array<{ identifier: string; transaction: string; transactionType: number }> }>
|
|
1178
|
-
}>("/mpc/txns/gen", body)
|
|
1179
|
-
|
|
1180
|
-
const txEntry = data.batches?.[0]?.transactions?.[0]
|
|
1181
|
-
if (!txEntry) throw new Error("Loopscale MPC co-signing returned no transaction")
|
|
1182
|
-
|
|
1183
|
-
const resultTx = VersionedTransaction.deserialize(Buffer.from(txEntry.transaction, "base64"))
|
|
1184
|
-
ensureMessageMatches(resultTx, transaction)
|
|
1185
|
-
return mergeInputSignatures(resultTx, transaction)
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
// Legacy endpoint
|
|
1189
|
-
const body = [{ transaction: serializedTx, message: serializedMsg }]
|
|
1190
|
-
const entries = await this.post<Array<{ message: string; signatures: Array<{ publicKey: string; signature: string }> }>>(
|
|
1191
|
-
"/markets/txn/gen", body, { "user-wallet": this.userWallet },
|
|
1192
|
-
)
|
|
1193
|
-
|
|
1194
|
-
if (!entries?.[0]) throw new Error("Loopscale co-signing returned no entries")
|
|
1195
|
-
const resultTx = buildTxFromLegacyEntry(entries[0])
|
|
1196
|
-
ensureMessageMatches(resultTx, transaction)
|
|
1197
|
-
return mergeInputSignatures(resultTx, transaction)
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
// ── Instruction helpers ──
|
|
1201
|
-
|
|
1202
|
-
classifyInstructions(instructions: TransactionInstruction[]): ClassifiedInstructions {
|
|
1203
|
-
const loopscale: LoopscaleGeneratedInstruction[] = []
|
|
1204
|
-
const topLevel: TransactionInstruction[] = []
|
|
1205
|
-
const setup: TransactionInstruction[] = []
|
|
1206
|
-
|
|
1207
|
-
for (const ix of instructions) {
|
|
1208
|
-
if (ix.programId.equals(LOOPSCALE_PROGRAM_ID)) {
|
|
1209
|
-
loopscale.push({ name: identifyInstruction(ix), instruction: ix })
|
|
1210
|
-
} else if (ix.programId.equals(ComputeBudgetProgram.programId)) {
|
|
1211
|
-
topLevel.push(ix)
|
|
1212
|
-
} else if (ix.programId.equals(ASSOCIATED_TOKEN_PROGRAM_ID)) {
|
|
1213
|
-
setup.push(ix)
|
|
1214
|
-
} else {
|
|
1215
|
-
// Oracle feeds, system program, etc. — top level
|
|
1216
|
-
topLevel.push(ix)
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
return { loopscale, topLevel, setup }
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
static identifyInstruction(ix: TransactionInstruction): LoopscaleInstructionName | null {
|
|
1224
|
-
return identifyInstruction(ix)
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
// ── Private helpers ──
|
|
1228
|
-
|
|
1229
|
-
private buildSyncActions(loopscaleIxs: LoopscaleGeneratedInstruction[]): LoopscaleInstruction[] {
|
|
1230
|
-
return loopscaleIxs
|
|
1231
|
-
.filter((g) => g.name !== null)
|
|
1232
|
-
.map((g) => {
|
|
1233
|
-
const wrapper = NAME_TO_ACTION_WRAPPER[g.name!]
|
|
1234
|
-
return wrapper(g.instruction)
|
|
1235
|
-
})
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
private buildRawBundle(
|
|
1239
|
-
transaction: VersionedTransaction,
|
|
1240
|
-
instructions: TransactionInstruction[],
|
|
1241
|
-
loopscaleInstructions: LoopscaleGeneratedInstruction[],
|
|
1242
|
-
): LoopscaleGeneratedInstructionBundle {
|
|
1243
|
-
return { transaction, instructions, loopscaleInstructions }
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
private toSingleTxResult(batch: LoopscaleStrategyBatch): LoopscaleTxResult {
|
|
1247
|
-
return {
|
|
1248
|
-
syncActions: batch.syncActions,
|
|
1249
|
-
topLevelInstructions: batch.topLevelInstructions,
|
|
1250
|
-
raw: batch.raw,
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
private async parseTransactionEntry(entry: LoopscaleLegacyTransactionEntry): Promise<LoopscaleStrategyBatch> {
|
|
1255
|
-
const tx = buildTxFromLegacyEntry(entry)
|
|
1256
|
-
const instructions = await decompileInstructions(this.connection, tx)
|
|
1257
|
-
const classified = this.classifyInstructions(instructions)
|
|
1258
|
-
const syncActions = this.buildSyncActions(classified.loopscale)
|
|
1259
|
-
const raw = this.buildRawBundle(tx, instructions, classified.loopscale)
|
|
1260
|
-
|
|
1261
|
-
return {
|
|
1262
|
-
syncActions,
|
|
1263
|
-
topLevelInstructions: classified.topLevel,
|
|
1264
|
-
setupInstructions: classified.setup,
|
|
1265
|
-
raw,
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
private async parseStrategyBatches(entries: LoopscaleLegacyTransactionEntry[]): Promise<LoopscaleStrategyBatch[]> {
|
|
1270
|
-
return Promise.all(entries.map((entry) => this.parseTransactionEntry(entry)))
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
private async fetchAndClassifyWrappedTx(
|
|
1274
|
-
path: string,
|
|
1275
|
-
body: unknown,
|
|
1276
|
-
headers: Record<string, string>,
|
|
1277
|
-
): Promise<LoopscaleTxResult> {
|
|
1278
|
-
const data = await this.post<{
|
|
1279
|
-
transactions: LoopscaleLegacyTransactionEntry[]
|
|
1280
|
-
expectedLoanInfo?: unknown
|
|
1281
|
-
}>(path, body, headers)
|
|
1282
|
-
|
|
1283
|
-
if (!data.transactions?.length) {
|
|
1284
|
-
throw new Error(`Loopscale ${path} returned no transactions`)
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
if (data.transactions.length > 1) {
|
|
1288
|
-
throw new Error(
|
|
1289
|
-
`Loopscale ${path} returned ${data.transactions.length} transactions — expected 1. ` +
|
|
1290
|
-
`Multi-transaction responses (e.g. from setupIxs) are not supported.`,
|
|
1291
|
-
)
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
const batch = await this.parseTransactionEntry(data.transactions[0])
|
|
1295
|
-
|
|
1296
|
-
if (this.debug) {
|
|
1297
|
-
console.log(
|
|
1298
|
-
`[LoopscaleClient] ${path}: syncActions=${batch.syncActions.length}, topLevel=${batch.topLevelInstructions.length}`,
|
|
1299
|
-
)
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
return this.toSingleTxResult(batch)
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
private async fetchAndClassifySingleTx(
|
|
1306
|
-
path: string,
|
|
1307
|
-
body: unknown,
|
|
1308
|
-
headers: Record<string, string>,
|
|
1309
|
-
): Promise<LoopscaleTxResult> {
|
|
1310
|
-
const entry = await this.post<LoopscaleLegacyTransactionEntry>(
|
|
1311
|
-
path, body, headers,
|
|
1312
|
-
)
|
|
1313
|
-
|
|
1314
|
-
const batch = await this.parseTransactionEntry(entry)
|
|
1315
|
-
|
|
1316
|
-
if (this.debug) {
|
|
1317
|
-
console.log(
|
|
1318
|
-
`[LoopscaleClient] ${path}: syncActions=${batch.syncActions.length}, topLevel=${batch.topLevelInstructions.length}`,
|
|
1319
|
-
)
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
return this.toSingleTxResult(batch)
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
private async post<T>(
|
|
1326
|
-
path: string,
|
|
1327
|
-
body?: unknown,
|
|
1328
|
-
extraHeaders?: Record<string, string>,
|
|
1329
|
-
): Promise<T> {
|
|
1330
|
-
const url = `${this.baseUrl}${path}`
|
|
1331
|
-
const headers: Record<string, string> = {
|
|
1332
|
-
"Content-Type": "application/json",
|
|
1333
|
-
...extraHeaders,
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
if (this.debug) {
|
|
1337
|
-
console.log(`\n[LoopscaleClient] POST ${url}`)
|
|
1338
|
-
if (body) console.log(` body: ${JSON.stringify(body, null, 2).split("\n").slice(0, 10).join("\n")}...`)
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
let response: Response | undefined
|
|
1342
|
-
const maxRetries = 3
|
|
1343
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1344
|
-
response = await fetch(url, {
|
|
1345
|
-
method: "POST",
|
|
1346
|
-
headers,
|
|
1347
|
-
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
1348
|
-
})
|
|
1349
|
-
|
|
1350
|
-
if (response.status === 429 && attempt < maxRetries) {
|
|
1351
|
-
const delay = 2000 * (attempt + 1)
|
|
1352
|
-
if (this.debug) console.log(` RATE LIMITED (429), retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})...`)
|
|
1353
|
-
await new Promise((r) => setTimeout(r, delay))
|
|
1354
|
-
continue
|
|
1355
|
-
}
|
|
1356
|
-
break
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
if (!response!.ok) {
|
|
1360
|
-
const text = await response!.text()
|
|
1361
|
-
if (this.debug) console.log(` ERROR ${response!.status}: ${text}`)
|
|
1362
|
-
throw new Error(`Loopscale API ${path} failed (${response!.status}): ${text}`)
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
const data = await response!.json() as T
|
|
1366
|
-
|
|
1367
|
-
if (this.debug) {
|
|
1368
|
-
console.log(` OK ${response!.status}`)
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
return data
|
|
1372
|
-
}
|
|
1373
|
-
}
|