@exponent-labs/exponent-sdk 0.9.0 → 0.9.2

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.
Files changed (155) hide show
  1. package/build/client/vaults/index.d.ts +2 -0
  2. package/build/client/vaults/index.js +2 -0
  3. package/build/client/vaults/index.js.map +1 -1
  4. package/build/client/vaults/types/index.d.ts +2 -0
  5. package/build/client/vaults/types/index.js +2 -0
  6. package/build/client/vaults/types/index.js.map +1 -1
  7. package/build/client/vaults/types/kaminoFarmEntry.d.ts +15 -0
  8. package/build/client/vaults/types/kaminoFarmEntry.js +17 -0
  9. package/build/client/vaults/types/kaminoFarmEntry.js.map +1 -0
  10. package/build/client/vaults/types/kaminoObligationEntry.d.ts +21 -4
  11. package/build/client/vaults/types/kaminoObligationEntry.js +2 -1
  12. package/build/client/vaults/types/kaminoObligationEntry.js.map +1 -1
  13. package/build/client/vaults/types/positionUpdate.d.ts +9 -0
  14. package/build/client/vaults/types/positionUpdate.js +23 -0
  15. package/build/client/vaults/types/positionUpdate.js.map +1 -1
  16. package/build/client/vaults/types/proposalAction.js +0 -3
  17. package/build/client/vaults/types/proposalAction.js.map +1 -1
  18. package/build/client/vaults/types/reserveFarmMapping.d.ts +19 -0
  19. package/build/client/vaults/types/reserveFarmMapping.js +18 -0
  20. package/build/client/vaults/types/reserveFarmMapping.js.map +1 -0
  21. package/build/client/vaults/types/strategyPosition.d.ts +5 -0
  22. package/build/client/vaults/types/strategyPosition.js +5 -0
  23. package/build/client/vaults/types/strategyPosition.js.map +1 -1
  24. package/build/exponentVaults/aumCalculator.d.ts +25 -4
  25. package/build/exponentVaults/aumCalculator.js +236 -15
  26. package/build/exponentVaults/aumCalculator.js.map +1 -1
  27. package/build/exponentVaults/fetcher.d.ts +52 -0
  28. package/build/exponentVaults/fetcher.js +199 -0
  29. package/build/exponentVaults/fetcher.js.map +1 -0
  30. package/build/exponentVaults/index.d.ts +10 -9
  31. package/build/exponentVaults/index.js +26 -8
  32. package/build/exponentVaults/index.js.map +1 -1
  33. package/build/exponentVaults/kamino-farms.d.ts +144 -0
  34. package/build/exponentVaults/kamino-farms.js +396 -0
  35. package/build/exponentVaults/kamino-farms.js.map +1 -0
  36. package/build/exponentVaults/loopscale/client.d.ts +240 -0
  37. package/build/exponentVaults/loopscale/client.js +590 -0
  38. package/build/exponentVaults/loopscale/client.js.map +1 -0
  39. package/build/exponentVaults/loopscale/client.test.d.ts +1 -0
  40. package/build/exponentVaults/loopscale/client.test.js +183 -0
  41. package/build/exponentVaults/loopscale/client.test.js.map +1 -0
  42. package/build/exponentVaults/loopscale/helpers.d.ts +29 -0
  43. package/build/exponentVaults/loopscale/helpers.js +119 -0
  44. package/build/exponentVaults/loopscale/helpers.js.map +1 -0
  45. package/build/exponentVaults/loopscale/index.d.ts +3 -0
  46. package/build/exponentVaults/loopscale/index.js +12 -0
  47. package/build/exponentVaults/loopscale/index.js.map +1 -0
  48. package/build/exponentVaults/loopscale/prepared-transactions.d.ts +13 -0
  49. package/build/exponentVaults/loopscale/prepared-transactions.js +271 -0
  50. package/build/exponentVaults/loopscale/prepared-transactions.js.map +1 -0
  51. package/build/exponentVaults/loopscale/prepared-transactions.test.d.ts +1 -0
  52. package/build/exponentVaults/loopscale/prepared-transactions.test.js +400 -0
  53. package/build/exponentVaults/loopscale/prepared-transactions.test.js.map +1 -0
  54. package/build/exponentVaults/loopscale/prepared-types.d.ts +62 -0
  55. package/build/exponentVaults/loopscale/prepared-types.js +3 -0
  56. package/build/exponentVaults/loopscale/prepared-types.js.map +1 -0
  57. package/build/exponentVaults/loopscale/response-plan.d.ts +69 -0
  58. package/build/exponentVaults/loopscale/response-plan.js +141 -0
  59. package/build/exponentVaults/loopscale/response-plan.js.map +1 -0
  60. package/build/exponentVaults/loopscale/response-plan.test.d.ts +1 -0
  61. package/build/exponentVaults/loopscale/response-plan.test.js +139 -0
  62. package/build/exponentVaults/loopscale/response-plan.test.js.map +1 -0
  63. package/build/exponentVaults/loopscale/send-plan.d.ts +75 -0
  64. package/build/exponentVaults/loopscale/send-plan.js +235 -0
  65. package/build/exponentVaults/loopscale/send-plan.js.map +1 -0
  66. package/build/exponentVaults/loopscale/types.d.ts +443 -0
  67. package/build/exponentVaults/loopscale/types.js +3 -0
  68. package/build/exponentVaults/loopscale/types.js.map +1 -0
  69. package/build/exponentVaults/loopscale-client.d.ts +113 -524
  70. package/build/exponentVaults/loopscale-client.js +296 -539
  71. package/build/exponentVaults/loopscale-client.js.map +1 -1
  72. package/build/exponentVaults/loopscale-client.test.d.ts +1 -0
  73. package/build/exponentVaults/loopscale-client.test.js +162 -0
  74. package/build/exponentVaults/loopscale-client.test.js.map +1 -0
  75. package/build/exponentVaults/loopscale-client.types.d.ts +425 -0
  76. package/build/exponentVaults/loopscale-client.types.js +3 -0
  77. package/build/exponentVaults/loopscale-client.types.js.map +1 -0
  78. package/build/exponentVaults/loopscale-execution.d.ts +125 -0
  79. package/build/exponentVaults/loopscale-execution.js +341 -0
  80. package/build/exponentVaults/loopscale-execution.js.map +1 -0
  81. package/build/exponentVaults/loopscale-execution.test.d.ts +1 -0
  82. package/build/exponentVaults/loopscale-execution.test.js +139 -0
  83. package/build/exponentVaults/loopscale-execution.test.js.map +1 -0
  84. package/build/exponentVaults/loopscale-vault.d.ts +115 -0
  85. package/build/exponentVaults/loopscale-vault.js +275 -0
  86. package/build/exponentVaults/loopscale-vault.js.map +1 -0
  87. package/build/exponentVaults/loopscale-vault.test.d.ts +1 -0
  88. package/build/exponentVaults/loopscale-vault.test.js +102 -0
  89. package/build/exponentVaults/loopscale-vault.test.js.map +1 -0
  90. package/build/exponentVaults/policyBuilders.d.ts +62 -0
  91. package/build/exponentVaults/policyBuilders.js +119 -2
  92. package/build/exponentVaults/policyBuilders.js.map +1 -1
  93. package/build/exponentVaults/pricePathResolver.d.ts +45 -0
  94. package/build/exponentVaults/pricePathResolver.js +198 -0
  95. package/build/exponentVaults/pricePathResolver.js.map +1 -0
  96. package/build/exponentVaults/pricePathResolver.test.d.ts +1 -0
  97. package/build/exponentVaults/pricePathResolver.test.js +369 -0
  98. package/build/exponentVaults/pricePathResolver.test.js.map +1 -0
  99. package/build/exponentVaults/syncTransaction.js +4 -1
  100. package/build/exponentVaults/syncTransaction.js.map +1 -1
  101. package/build/exponentVaults/titan-quote.js +170 -36
  102. package/build/exponentVaults/titan-quote.js.map +1 -1
  103. package/build/exponentVaults/vault-instruction-types.d.ts +363 -0
  104. package/build/exponentVaults/vault-instruction-types.js +128 -0
  105. package/build/exponentVaults/vault-instruction-types.js.map +1 -0
  106. package/build/exponentVaults/vault-interaction.d.ts +203 -343
  107. package/build/exponentVaults/vault-interaction.js +1894 -426
  108. package/build/exponentVaults/vault-interaction.js.map +1 -1
  109. package/build/exponentVaults/vault-interaction.kamino-vault.test.d.ts +1 -0
  110. package/build/exponentVaults/vault-interaction.kamino-vault.test.js +143 -0
  111. package/build/exponentVaults/vault-interaction.kamino-vault.test.js.map +1 -0
  112. package/build/exponentVaults/vault.d.ts +51 -2
  113. package/build/exponentVaults/vault.js +324 -48
  114. package/build/exponentVaults/vault.js.map +1 -1
  115. package/build/exponentVaults/vaultTransactionBuilder.d.ts +100 -134
  116. package/build/exponentVaults/vaultTransactionBuilder.js +383 -285
  117. package/build/exponentVaults/vaultTransactionBuilder.js.map +1 -1
  118. package/build/exponentVaults/vaultTransactionBuilder.test.d.ts +1 -0
  119. package/build/exponentVaults/vaultTransactionBuilder.test.js +297 -0
  120. package/build/exponentVaults/vaultTransactionBuilder.test.js.map +1 -0
  121. package/build/marketThree.d.ts +6 -2
  122. package/build/marketThree.js +10 -8
  123. package/build/marketThree.js.map +1 -1
  124. package/package.json +34 -32
  125. package/src/client/vaults/index.ts +2 -0
  126. package/src/client/vaults/types/index.ts +2 -0
  127. package/src/client/vaults/types/kaminoFarmEntry.ts +32 -0
  128. package/src/client/vaults/types/kaminoObligationEntry.ts +6 -3
  129. package/src/client/vaults/types/positionUpdate.ts +62 -0
  130. package/src/client/vaults/types/proposalAction.ts +0 -3
  131. package/src/client/vaults/types/reserveFarmMapping.ts +35 -0
  132. package/src/client/vaults/types/strategyPosition.ts +18 -1
  133. package/src/exponentVaults/aumCalculator.ts +353 -16
  134. package/src/exponentVaults/fetcher.ts +257 -0
  135. package/src/exponentVaults/index.ts +65 -40
  136. package/src/exponentVaults/kamino-farms.ts +538 -0
  137. package/src/exponentVaults/loopscale/client.ts +808 -0
  138. package/src/exponentVaults/loopscale/helpers.ts +172 -0
  139. package/src/exponentVaults/loopscale/index.ts +57 -0
  140. package/src/exponentVaults/loopscale/prepared-transactions.ts +435 -0
  141. package/src/exponentVaults/loopscale/prepared-types.ts +73 -0
  142. package/src/exponentVaults/loopscale/types.ts +466 -0
  143. package/src/exponentVaults/policyBuilders.ts +170 -0
  144. package/src/exponentVaults/pricePathResolver.test.ts +466 -0
  145. package/src/exponentVaults/pricePathResolver.ts +273 -0
  146. package/src/exponentVaults/syncTransaction.ts +6 -1
  147. package/src/exponentVaults/titan-quote.ts +231 -45
  148. package/src/exponentVaults/vault-instruction-types.ts +493 -0
  149. package/src/exponentVaults/vault-interaction.kamino-vault.test.ts +149 -0
  150. package/src/exponentVaults/vault-interaction.ts +2818 -799
  151. package/src/exponentVaults/vault.ts +474 -63
  152. package/src/exponentVaults/vaultTransactionBuilder.test.ts +349 -0
  153. package/src/exponentVaults/vaultTransactionBuilder.ts +581 -433
  154. package/src/marketThree.ts +14 -6
  155. 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
- }