@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.
Files changed (153) 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 +24 -7
  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/obligationType.d.ts +1 -1
  14. package/build/client/vaults/types/positionUpdate.d.ts +9 -0
  15. package/build/client/vaults/types/positionUpdate.js +23 -0
  16. package/build/client/vaults/types/positionUpdate.js.map +1 -1
  17. package/build/client/vaults/types/proposalAction.d.ts +54 -54
  18. package/build/client/vaults/types/proposalAction.js +0 -3
  19. package/build/client/vaults/types/proposalAction.js.map +1 -1
  20. package/build/client/vaults/types/reserveFarmMapping.d.ts +19 -0
  21. package/build/client/vaults/types/reserveFarmMapping.js +18 -0
  22. package/build/client/vaults/types/reserveFarmMapping.js.map +1 -0
  23. package/build/client/vaults/types/strategyPosition.d.ts +6 -1
  24. package/build/client/vaults/types/strategyPosition.js +5 -0
  25. package/build/client/vaults/types/strategyPosition.js.map +1 -1
  26. package/build/exponentVaults/aumCalculator.d.ts +25 -4
  27. package/build/exponentVaults/aumCalculator.js +236 -15
  28. package/build/exponentVaults/aumCalculator.js.map +1 -1
  29. package/build/exponentVaults/fetcher.d.ts +52 -0
  30. package/build/exponentVaults/fetcher.js +199 -0
  31. package/build/exponentVaults/fetcher.js.map +1 -0
  32. package/build/exponentVaults/index.d.ts +10 -9
  33. package/build/exponentVaults/index.js +25 -8
  34. package/build/exponentVaults/index.js.map +1 -1
  35. package/build/exponentVaults/kamino-farms.d.ts +144 -0
  36. package/build/exponentVaults/kamino-farms.js +396 -0
  37. package/build/exponentVaults/kamino-farms.js.map +1 -0
  38. package/build/exponentVaults/loopscale/client.d.ts +240 -0
  39. package/build/exponentVaults/loopscale/client.js +590 -0
  40. package/build/exponentVaults/loopscale/client.js.map +1 -0
  41. package/build/exponentVaults/loopscale/client.test.d.ts +1 -0
  42. package/build/exponentVaults/loopscale/client.test.js +183 -0
  43. package/build/exponentVaults/loopscale/client.test.js.map +1 -0
  44. package/build/exponentVaults/loopscale/helpers.d.ts +29 -0
  45. package/build/exponentVaults/loopscale/helpers.js +119 -0
  46. package/build/exponentVaults/loopscale/helpers.js.map +1 -0
  47. package/build/exponentVaults/loopscale/index.d.ts +3 -0
  48. package/build/exponentVaults/loopscale/index.js +12 -0
  49. package/build/exponentVaults/loopscale/index.js.map +1 -0
  50. package/build/exponentVaults/loopscale/prepared-transactions.d.ts +13 -0
  51. package/build/exponentVaults/loopscale/prepared-transactions.js +271 -0
  52. package/build/exponentVaults/loopscale/prepared-transactions.js.map +1 -0
  53. package/build/exponentVaults/loopscale/prepared-transactions.test.d.ts +1 -0
  54. package/build/exponentVaults/loopscale/prepared-transactions.test.js +400 -0
  55. package/build/exponentVaults/loopscale/prepared-transactions.test.js.map +1 -0
  56. package/build/exponentVaults/loopscale/prepared-types.d.ts +62 -0
  57. package/build/exponentVaults/loopscale/prepared-types.js +3 -0
  58. package/build/exponentVaults/loopscale/prepared-types.js.map +1 -0
  59. package/build/exponentVaults/loopscale/response-plan.d.ts +69 -0
  60. package/build/exponentVaults/loopscale/response-plan.js +141 -0
  61. package/build/exponentVaults/loopscale/response-plan.js.map +1 -0
  62. package/build/exponentVaults/loopscale/response-plan.test.d.ts +1 -0
  63. package/build/exponentVaults/loopscale/response-plan.test.js +139 -0
  64. package/build/exponentVaults/loopscale/response-plan.test.js.map +1 -0
  65. package/build/exponentVaults/loopscale/send-plan.d.ts +75 -0
  66. package/build/exponentVaults/loopscale/send-plan.js +235 -0
  67. package/build/exponentVaults/loopscale/send-plan.js.map +1 -0
  68. package/build/exponentVaults/loopscale/types.d.ts +443 -0
  69. package/build/exponentVaults/loopscale/types.js +3 -0
  70. package/build/exponentVaults/loopscale/types.js.map +1 -0
  71. package/build/exponentVaults/loopscale-client.d.ts +113 -524
  72. package/build/exponentVaults/loopscale-client.js +296 -539
  73. package/build/exponentVaults/loopscale-client.js.map +1 -1
  74. package/build/exponentVaults/loopscale-client.test.d.ts +1 -0
  75. package/build/exponentVaults/loopscale-client.test.js +162 -0
  76. package/build/exponentVaults/loopscale-client.test.js.map +1 -0
  77. package/build/exponentVaults/loopscale-client.types.d.ts +425 -0
  78. package/build/exponentVaults/loopscale-client.types.js +3 -0
  79. package/build/exponentVaults/loopscale-client.types.js.map +1 -0
  80. package/build/exponentVaults/loopscale-execution.d.ts +125 -0
  81. package/build/exponentVaults/loopscale-execution.js +341 -0
  82. package/build/exponentVaults/loopscale-execution.js.map +1 -0
  83. package/build/exponentVaults/loopscale-execution.test.d.ts +1 -0
  84. package/build/exponentVaults/loopscale-execution.test.js +139 -0
  85. package/build/exponentVaults/loopscale-execution.test.js.map +1 -0
  86. package/build/exponentVaults/loopscale-vault.d.ts +115 -0
  87. package/build/exponentVaults/loopscale-vault.js +275 -0
  88. package/build/exponentVaults/loopscale-vault.js.map +1 -0
  89. package/build/exponentVaults/loopscale-vault.test.d.ts +1 -0
  90. package/build/exponentVaults/loopscale-vault.test.js +102 -0
  91. package/build/exponentVaults/loopscale-vault.test.js.map +1 -0
  92. package/build/exponentVaults/policyBuilders.d.ts +62 -0
  93. package/build/exponentVaults/policyBuilders.js +119 -2
  94. package/build/exponentVaults/policyBuilders.js.map +1 -1
  95. package/build/exponentVaults/pricePathResolver.d.ts +45 -0
  96. package/build/exponentVaults/pricePathResolver.js +198 -0
  97. package/build/exponentVaults/pricePathResolver.js.map +1 -0
  98. package/build/exponentVaults/pricePathResolver.test.d.ts +1 -0
  99. package/build/exponentVaults/pricePathResolver.test.js +369 -0
  100. package/build/exponentVaults/pricePathResolver.test.js.map +1 -0
  101. package/build/exponentVaults/syncTransaction.js +4 -1
  102. package/build/exponentVaults/syncTransaction.js.map +1 -1
  103. package/build/exponentVaults/titan-quote.js +170 -36
  104. package/build/exponentVaults/titan-quote.js.map +1 -1
  105. package/build/exponentVaults/vault-instruction-types.d.ts +363 -0
  106. package/build/exponentVaults/vault-instruction-types.js +128 -0
  107. package/build/exponentVaults/vault-instruction-types.js.map +1 -0
  108. package/build/exponentVaults/vault-interaction.d.ts +156 -313
  109. package/build/exponentVaults/vault-interaction.js +1581 -353
  110. package/build/exponentVaults/vault-interaction.js.map +1 -1
  111. package/build/exponentVaults/vault.d.ts +51 -2
  112. package/build/exponentVaults/vault.js +324 -48
  113. package/build/exponentVaults/vault.js.map +1 -1
  114. package/build/exponentVaults/vaultTransactionBuilder.d.ts +100 -134
  115. package/build/exponentVaults/vaultTransactionBuilder.js +359 -266
  116. package/build/exponentVaults/vaultTransactionBuilder.js.map +1 -1
  117. package/build/exponentVaults/vaultTransactionBuilder.test.d.ts +1 -0
  118. package/build/exponentVaults/vaultTransactionBuilder.test.js +214 -0
  119. package/build/exponentVaults/vaultTransactionBuilder.test.js.map +1 -0
  120. package/build/marketThree.d.ts +6 -2
  121. package/build/marketThree.js +10 -8
  122. package/build/marketThree.js.map +1 -1
  123. package/package.json +32 -32
  124. package/src/client/vaults/index.ts +2 -0
  125. package/src/client/vaults/types/index.ts +2 -0
  126. package/src/client/vaults/types/kaminoFarmEntry.ts +32 -0
  127. package/src/client/vaults/types/kaminoObligationEntry.ts +6 -3
  128. package/src/client/vaults/types/positionUpdate.ts +62 -0
  129. package/src/client/vaults/types/proposalAction.ts +0 -3
  130. package/src/client/vaults/types/reserveFarmMapping.ts +35 -0
  131. package/src/client/vaults/types/strategyPosition.ts +18 -1
  132. package/src/exponentVaults/aumCalculator.ts +353 -16
  133. package/src/exponentVaults/fetcher.ts +257 -0
  134. package/src/exponentVaults/index.ts +64 -40
  135. package/src/exponentVaults/kamino-farms.ts +538 -0
  136. package/src/exponentVaults/loopscale/client.ts +808 -0
  137. package/src/exponentVaults/loopscale/helpers.ts +172 -0
  138. package/src/exponentVaults/loopscale/index.ts +57 -0
  139. package/src/exponentVaults/loopscale/prepared-transactions.ts +435 -0
  140. package/src/exponentVaults/loopscale/prepared-types.ts +73 -0
  141. package/src/exponentVaults/loopscale/types.ts +466 -0
  142. package/src/exponentVaults/policyBuilders.ts +170 -0
  143. package/src/exponentVaults/pricePathResolver.test.ts +466 -0
  144. package/src/exponentVaults/pricePathResolver.ts +273 -0
  145. package/src/exponentVaults/syncTransaction.ts +6 -1
  146. package/src/exponentVaults/titan-quote.ts +231 -45
  147. package/src/exponentVaults/vault-instruction-types.ts +493 -0
  148. package/src/exponentVaults/vault-interaction.ts +2227 -636
  149. package/src/exponentVaults/vault.ts +474 -63
  150. package/src/exponentVaults/vaultTransactionBuilder.test.ts +256 -0
  151. package/src/exponentVaults/vaultTransactionBuilder.ts +555 -413
  152. package/src/marketThree.ts +14 -6
  153. package/src/exponentVaults/loopscale-client.ts +0 -1373
@@ -0,0 +1,808 @@
1
+ /**
2
+ * Loopscale manager flow:
3
+ *
4
+ * 1. Call a raw Loopscale endpoint such as `createStrategy(...)`,
5
+ * `createLoan(...)`, or `borrowPrincipal(...)`.
6
+ * 2. Hand the raw response back to this client.
7
+ * 3. Choose one of two paths:
8
+ * - `prepareVaultTransactions(...)` when you want appendable instruction
9
+ * pieces you can compose with your own transaction building logic.
10
+ * - `buildVaultTransactions(...)` when you want locally signed
11
+ * `VersionedTransaction`s that are ready for optional Loopscale
12
+ * co-signing and then `connection.sendTransaction(...)`.
13
+ * 4. Call `coSign(...)` only on returned transactions where
14
+ * `requiresLoopscaleCoSign === true`.
15
+ *
16
+ * This distinction is intentional:
17
+ *
18
+ * - Wrapped Loopscale sync transactions require Loopscale MPC because they
19
+ * represent Loopscale-authored flows that expect Loopscale's signature.
20
+ * - Setup transactions and other top-level non-Loopscale transactions must
21
+ * stay local. They are constructed by the SDK to make the manager flow
22
+ * complete, but they are not Loopscale-signed transactions and should not
23
+ * be sent to Loopscale's MPC endpoint.
24
+ *
25
+ * In other words, `requiresLoopscaleCoSign` is not a convenience flag. It is
26
+ * the contract that tells the caller which transactions belong to Loopscale's
27
+ * signing domain and which ones do not.
28
+ *
29
+ * Example:
30
+ *
31
+ * ```ts
32
+ * const client = new LoopscaleClient({
33
+ * connection,
34
+ * userWallet: squadsVault,
35
+ * payer: squadsVault,
36
+ * })
37
+ *
38
+ * const response = await client.createStrategy({
39
+ * principalMint,
40
+ * lender: squadsVault,
41
+ * amount: 0,
42
+ * })
43
+ *
44
+ * const builtTransactions = await client.buildVaultTransactions({
45
+ * response,
46
+ * context: {
47
+ * connection,
48
+ * signer: manager.publicKey,
49
+ * signers: [manager],
50
+ * vaultPda: squadsVault,
51
+ * vaultAddress,
52
+ * squadsProgram,
53
+ * },
54
+ * })
55
+ *
56
+ * for (const built of builtTransactions) {
57
+ * const transaction = built.requiresLoopscaleCoSign
58
+ * ? await client.coSign(built.transaction)
59
+ * : built.transaction
60
+ *
61
+ * await connection.sendTransaction(transaction, { skipPreflight: false })
62
+ * }
63
+ * ```
64
+ */
65
+ import {
66
+ AddressLookupTableAccount,
67
+ Connection,
68
+ PublicKey,
69
+ TransactionMessage,
70
+ type TransactionInstruction,
71
+ type Signer,
72
+ VersionedTransaction,
73
+ } from "@solana/web3.js"
74
+
75
+ import type {
76
+ LoopscaleBorrowPrincipalParams,
77
+ LoopscaleClientConfig,
78
+ LoopscaleCloseLoanParams,
79
+ LoopscaleCloseStrategyParams,
80
+ LoopscaleCreateLoanParams,
81
+ LoopscaleCreateLoanResponse,
82
+ LoopscaleCreateStrategyParams,
83
+ LoopscaleDepositCollateralParams,
84
+ LoopscaleDepositStrategyParams,
85
+ LoopscaleGetStrategiesParams,
86
+ LoopscaleLoanInfoParams,
87
+ LoopscaleLoanInfoResponse,
88
+ LoopscaleLoanTransactionResponse,
89
+ LoopscaleMaxQuote,
90
+ LoopscaleMaxQuoteParams,
91
+ LoopscaleQuote,
92
+ LoopscaleQuoteParams,
93
+ LoopscaleRepayLoanSimpleParams,
94
+ LoopscaleStrategyInfoResponse,
95
+ LoopscaleTransactionResponse,
96
+ LoopscaleTransactionsResponse,
97
+ LoopscaleUpdateStrategyParams,
98
+ LoopscaleVersionedTransactionBatchResponse,
99
+ LoopscaleVersionedTransactionResponse,
100
+ LoopscaleWithdrawCollateralParams,
101
+ LoopscaleWithdrawStrategyParams,
102
+ } from "./types"
103
+ import type {
104
+ LoopscaleBuildTransactionsContext,
105
+ LoopscaleBuiltTransaction,
106
+ LoopscalePreparedTransaction,
107
+ LoopscaleVaultPreparationContext,
108
+ } from "./prepared-types"
109
+ import {
110
+ deserializeLoopscaleTransactionBatchResponse,
111
+ deserializeLoopscaleTransactionResponse,
112
+ extractCreatedStrategyAddress,
113
+ extractCreatedStrategyNonce,
114
+ getLoopscaleTransactionResponses,
115
+ identifyLoopscaleInstruction,
116
+ } from "./helpers"
117
+ import { prepareLoopscaleVaultTransactions } from "./prepared-transactions"
118
+
119
+ const LOOPSCALE_API_BASE_URL = "https://tars.loopscale.com/v1"
120
+
121
+ function toBase58(value: PublicKey | string): string {
122
+ return typeof value === "string" ? value : value.toBase58()
123
+ }
124
+
125
+ function toSafeNumber(value: bigint | number, field: string): number {
126
+ if (typeof value === "bigint") {
127
+ if (value < 0n) throw new Error(`${field} must be non-negative`)
128
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`${field} is too large for a JSON number`)
129
+ return Number(value)
130
+ }
131
+
132
+ if (!Number.isInteger(value) || value < 0) {
133
+ throw new Error(`${field} must be a non-negative integer`)
134
+ }
135
+
136
+ return value
137
+ }
138
+
139
+ function hasSignature(signature: Uint8Array): boolean {
140
+ return signature.some((value) => value !== 0)
141
+ }
142
+
143
+ function withPayerHeader(
144
+ headers: Record<string, string>,
145
+ payer: string | undefined,
146
+ ): Record<string, string> {
147
+ if (!payer) return headers
148
+ return { ...headers, payer }
149
+ }
150
+
151
+ function mergeCoSignedTransaction(
152
+ transaction: VersionedTransaction,
153
+ coSignedTransaction: VersionedTransaction,
154
+ ): VersionedTransaction {
155
+ const left = Buffer.from(transaction.message.serialize())
156
+ const right = Buffer.from(coSignedTransaction.message.serialize())
157
+ if (!left.equals(right)) {
158
+ throw new Error("Loopscale returned a different transaction message; refusing to merge signatures")
159
+ }
160
+
161
+ const signerKeys = coSignedTransaction.message.staticAccountKeys
162
+ .slice(0, coSignedTransaction.message.header.numRequiredSignatures)
163
+ const inputSignerKeys = transaction.message.staticAccountKeys
164
+ .slice(0, transaction.message.header.numRequiredSignatures)
165
+
166
+ for (let index = 0; index < inputSignerKeys.length; index += 1) {
167
+ const inputSignature = transaction.signatures[index]
168
+ if (!hasSignature(inputSignature)) continue
169
+
170
+ const signerIndex = signerKeys.findIndex((candidate) => candidate.equals(inputSignerKeys[index]!))
171
+ if (signerIndex >= 0 && !hasSignature(coSignedTransaction.signatures[signerIndex]!)) {
172
+ coSignedTransaction.signatures[signerIndex] = inputSignature
173
+ }
174
+ }
175
+
176
+ return coSignedTransaction
177
+ }
178
+
179
+ function dedupeLookupTableAccounts(
180
+ lookupTableAccounts: AddressLookupTableAccount[],
181
+ ): AddressLookupTableAccount[] {
182
+ const seen = new Set<string>()
183
+ return lookupTableAccounts.filter((account) => {
184
+ const key = account.key.toBase58()
185
+ if (seen.has(key)) {
186
+ return false
187
+ }
188
+ seen.add(key)
189
+ return true
190
+ })
191
+ }
192
+
193
+ function dedupeSigners(signers: Signer[]): Signer[] {
194
+ const seen = new Set<string>()
195
+ return signers.filter((signer) => {
196
+ const key = signer.publicKey.toBase58()
197
+ if (seen.has(key)) {
198
+ return false
199
+ }
200
+ seen.add(key)
201
+ return true
202
+ })
203
+ }
204
+
205
+ async function resolveLookupTableAccounts(
206
+ connection: Connection,
207
+ addresses: readonly PublicKey[],
208
+ ): Promise<AddressLookupTableAccount[]> {
209
+ const results = await Promise.all(
210
+ addresses.map((address) => connection.getAddressLookupTable(address)),
211
+ )
212
+
213
+ return dedupeLookupTableAccounts(
214
+ results
215
+ .map((result) => result.value)
216
+ .filter((value): value is AddressLookupTableAccount => value !== null),
217
+ )
218
+ }
219
+
220
+ async function buildSignedTransaction(
221
+ connection: Connection,
222
+ signer: PublicKey,
223
+ instructions: readonly TransactionInstruction[],
224
+ lookupTableAddresses: readonly PublicKey[],
225
+ signers: Signer[],
226
+ ): Promise<VersionedTransaction> {
227
+ const addressLookupTableAccounts = await resolveLookupTableAccounts(connection, lookupTableAddresses)
228
+ const { blockhash } = await connection.getLatestBlockhash("confirmed")
229
+ const message = new TransactionMessage({
230
+ payerKey: signer,
231
+ recentBlockhash: blockhash,
232
+ instructions: [...instructions],
233
+ }).compileToV0Message(addressLookupTableAccounts)
234
+
235
+ const transaction = new VersionedTransaction(message)
236
+ transaction.sign(dedupeSigners(signers))
237
+ return transaction
238
+ }
239
+
240
+ /**
241
+ * Thin docs-aligned Loopscale client for strategies, loans, and related query endpoints.
242
+ *
243
+ * Transaction methods intentionally return the raw Loopscale response shapes
244
+ * instead of derived sync-action bundles. Use
245
+ * {@link LoopscaleClient.prepareVaultTransactions} when you need appendable
246
+ * transaction pieces, or {@link LoopscaleClient.buildVaultTransactions} when
247
+ * you want locally signed transactions that are ready for optional
248
+ * `client.coSign(...)` and `connection.sendTransaction(...)`.
249
+ *
250
+ * Refinancing and other lock/unlock maintenance flows remain intentionally
251
+ * excluded. The client only covers strategies, loans, and cleanup endpoints
252
+ * that can be represented as raw transaction responses.
253
+ *
254
+ * Typical usage:
255
+ *
256
+ * - Query Loopscale data through the read methods on this client.
257
+ * - Build raw Loopscale transactions through the strategy and loan methods.
258
+ * - Turn raw responses into appendable instruction pieces with
259
+ * {@link LoopscaleClient.prepareVaultTransactions}.
260
+ * - Or compile ready-to-send transactions with
261
+ * {@link LoopscaleClient.buildVaultTransactions}.
262
+ * - Call {@link LoopscaleClient.coSign} only for transactions flagged with
263
+ * `requiresLoopscaleCoSign`.
264
+ */
265
+ export class LoopscaleClient {
266
+ private readonly connection: Connection
267
+ private readonly baseUrl: string
268
+ private readonly userWallet: string
269
+ private readonly payer: string
270
+ private readonly debug: boolean
271
+
272
+ constructor(config: LoopscaleClientConfig) {
273
+ this.connection = config.connection
274
+ this.baseUrl = config.baseUrl ?? LOOPSCALE_API_BASE_URL
275
+ this.userWallet = toBase58(config.userWallet)
276
+ this.payer = toBase58(config.payer ?? config.userWallet)
277
+ this.debug = config.debug ?? false
278
+ }
279
+
280
+ /**
281
+ * Load Loopscale quotes for a principal/collateral pair.
282
+ */
283
+ async getQuotes(params: LoopscaleQuoteParams): Promise<LoopscaleQuote[]> {
284
+ const body: Record<string, unknown> = {
285
+ durationType: params.durationType,
286
+ duration: params.duration,
287
+ principal: toBase58(params.principal),
288
+ collateral: params.collateral.map(toBase58),
289
+ limit: params.limit ?? 10,
290
+ offset: params.offset ?? 0,
291
+ }
292
+
293
+ if (params.minPrincipalAmount !== undefined) {
294
+ body.minPrincipalAmount = toSafeNumber(params.minPrincipalAmount, "minPrincipalAmount")
295
+ }
296
+
297
+ return this.post<LoopscaleQuote[]>("/markets/quote", body, {
298
+ "user-wallet": this.userWallet,
299
+ })
300
+ }
301
+
302
+ /**
303
+ * Load the best quote for one or more specific collateral assets.
304
+ */
305
+ async getMaxQuote(params: LoopscaleMaxQuoteParams): Promise<LoopscaleMaxQuote[]> {
306
+ return this.post<LoopscaleMaxQuote[]>("/markets/quote/max", {
307
+ durationType: params.durationType,
308
+ duration: params.duration,
309
+ principalMint: toBase58(params.principalMint),
310
+ collateralFilter: params.collateralFilter.map((item) => ({
311
+ amount: toSafeNumber(item.amount, "amount"),
312
+ assetData: item.assetData,
313
+ })),
314
+ }, {
315
+ "user-wallet": this.userWallet,
316
+ })
317
+ }
318
+
319
+ /**
320
+ * Load Loopscale loan positions.
321
+ */
322
+ async getLoanInfo(params: LoopscaleLoanInfoParams): Promise<LoopscaleLoanInfoResponse> {
323
+ return this.post<LoopscaleLoanInfoResponse>("/markets/loans/info", params)
324
+ }
325
+
326
+ /**
327
+ * Load Loopscale lending strategies.
328
+ */
329
+ async getStrategies(params: LoopscaleGetStrategiesParams): Promise<LoopscaleStrategyInfoResponse> {
330
+ return this.post<LoopscaleStrategyInfoResponse>("/markets/strategy/infos", params)
331
+ }
332
+
333
+ /**
334
+ * Build a raw Loopscale `createStrategy` transaction batch.
335
+ */
336
+ async createStrategy(
337
+ params: LoopscaleCreateStrategyParams,
338
+ ): Promise<LoopscaleVersionedTransactionBatchResponse> {
339
+ const body: Record<string, unknown> = {
340
+ principalMint: toBase58(params.principalMint),
341
+ lender: toBase58(params.lender),
342
+ amount: toSafeNumber(params.amount, "amount"),
343
+ }
344
+
345
+ if (params.originationsEnabled !== undefined) body.originationsEnabled = params.originationsEnabled
346
+ if (params.liquidityBuffer !== undefined) body.liquidityBuffer = params.liquidityBuffer
347
+ if (params.interestFee !== undefined) body.interestFee = params.interestFee
348
+ if (params.originationFee !== undefined) body.originationFee = params.originationFee
349
+ if (params.originationCap !== undefined) body.originationCap = params.originationCap
350
+ if (params.collateralTerms) body.collateralTerms = params.collateralTerms
351
+ if (params.marketInformation) body.marketInformation = toBase58(params.marketInformation)
352
+ if (params.externalYieldSourceArgs) body.externalYieldSourceArgs = params.externalYieldSourceArgs
353
+
354
+ return this.post<LoopscaleVersionedTransactionBatchResponse>(
355
+ "/markets/strategy/create",
356
+ body,
357
+ { payer: this.payer },
358
+ )
359
+ }
360
+
361
+ /**
362
+ * Build a raw Loopscale `depositStrategy` transaction.
363
+ */
364
+ async depositStrategy(
365
+ params: LoopscaleDepositStrategyParams,
366
+ ): Promise<LoopscaleVersionedTransactionResponse> {
367
+ return this.post<LoopscaleVersionedTransactionResponse>("/markets/strategy/deposit", {
368
+ strategy: toBase58(params.strategy),
369
+ amount: toSafeNumber(params.amount, "amount"),
370
+ }, withPayerHeader({
371
+ "User-Wallet": this.userWallet,
372
+ }, this.payer))
373
+ }
374
+
375
+ /**
376
+ * Build a raw Loopscale `withdrawStrategy` transaction.
377
+ */
378
+ async withdrawStrategy(
379
+ params: LoopscaleWithdrawStrategyParams,
380
+ ): Promise<LoopscaleVersionedTransactionResponse> {
381
+ return this.post<LoopscaleVersionedTransactionResponse>("/markets/strategy/withdraw", {
382
+ strategy: toBase58(params.strategy),
383
+ amount: toSafeNumber(params.amount, "amount"),
384
+ withdrawAll: params.withdrawAll,
385
+ }, withPayerHeader({
386
+ "User-Wallet": this.userWallet,
387
+ }, this.payer))
388
+ }
389
+
390
+ /**
391
+ * Build a raw Loopscale `closeStrategy` transaction.
392
+ */
393
+ async closeStrategy(
394
+ params: LoopscaleCloseStrategyParams,
395
+ ): Promise<LoopscaleVersionedTransactionResponse> {
396
+ return this.post<LoopscaleVersionedTransactionResponse>(
397
+ `/markets/strategy/close/${toBase58(params.strategy)}`,
398
+ undefined,
399
+ { payer: this.payer },
400
+ )
401
+ }
402
+
403
+ /**
404
+ * Build raw Loopscale `updateStrategy` transactions.
405
+ */
406
+ async updateStrategy(
407
+ params: LoopscaleUpdateStrategyParams,
408
+ ): Promise<LoopscaleVersionedTransactionBatchResponse> {
409
+ const body: Record<string, unknown> = {
410
+ strategy: toBase58(params.strategy),
411
+ }
412
+
413
+ if (params.collateralTerms) body.collateralTerms = params.collateralTerms
414
+ if (params.updateParams) body.updateParams = params.updateParams
415
+
416
+ return this.post<LoopscaleVersionedTransactionBatchResponse>(
417
+ "/markets/strategy/update",
418
+ body,
419
+ withPayerHeader({
420
+ "User-Wallet": this.userWallet,
421
+ }, this.payer),
422
+ )
423
+ }
424
+
425
+ /**
426
+ * Build a raw Loopscale `createLoan` transaction.
427
+ */
428
+ async createLoan(
429
+ params: LoopscaleCreateLoanParams,
430
+ ): Promise<LoopscaleCreateLoanResponse> {
431
+ const body: Record<string, unknown> = {
432
+ borrower: toBase58(params.borrower),
433
+ depositCollateral: params.depositCollateral.map((item) => ({
434
+ collateralAmount: toSafeNumber(item.collateralAmount, "collateralAmount"),
435
+ collateralAssetData: item.collateralAssetData,
436
+ })),
437
+ principalRequested: params.principalRequested.map((item) => ({
438
+ ledgerIndex: item.ledgerIndex,
439
+ principalAmount: toSafeNumber(item.principalAmount, "principalAmount"),
440
+ principalMint: toBase58(item.principalMint),
441
+ strategy: toBase58(item.strategy),
442
+ durationIndex: item.durationIndex,
443
+ expectedLoanValues: item.expectedLoanValues ?? { expectedApy: 0, expectedLqt: [0, 0, 0, 0, 0] },
444
+ })),
445
+ }
446
+
447
+ if (params.assetIndexGuidance) body.assetIndexGuidance = params.assetIndexGuidance
448
+ if (params.loanNonce) body.loanNonce = params.loanNonce
449
+ if (params.isLoop !== undefined) body.isLoop = params.isLoop
450
+
451
+ return this.post<LoopscaleCreateLoanResponse>(
452
+ "/markets/creditbook/create",
453
+ body,
454
+ { payer: this.payer },
455
+ )
456
+ }
457
+
458
+ /**
459
+ * Build a raw Loopscale `borrowPrincipal` transaction.
460
+ *
461
+ * The published OpenAPI schema currently documents a single serialized
462
+ * transaction response, but live responses can also return a wrapped
463
+ * `transactions` array with `expectedLoanInfo` when Loopscale inserts
464
+ * additional internal maintenance steps.
465
+ */
466
+ async borrowPrincipal(
467
+ params: LoopscaleBorrowPrincipalParams,
468
+ ): Promise<LoopscaleLoanTransactionResponse> {
469
+ const body: Record<string, unknown> = {
470
+ loan: toBase58(params.loan),
471
+ borrowParams: {
472
+ amount: toSafeNumber(params.borrowParams.amount, "amount"),
473
+ duration: params.borrowParams.durationIndex,
474
+ },
475
+ strategy: toBase58(params.strategy),
476
+ }
477
+
478
+ if (params.borrowParams.expectedLoanValues) {
479
+ body.borrowParams = {
480
+ ...body.borrowParams as Record<string, unknown>,
481
+ expectedLoanValues: params.borrowParams.expectedLoanValues,
482
+ }
483
+ }
484
+ if (params.refinanceParams) body.refinanceParams = params.refinanceParams
485
+ if (params.isLoop !== undefined) body.isLoop = params.isLoop
486
+
487
+ return this.post<LoopscaleLoanTransactionResponse>(
488
+ "/markets/creditbook/borrow",
489
+ body,
490
+ { payer: this.payer },
491
+ )
492
+ }
493
+
494
+ /**
495
+ * Build a raw Loopscale `depositCollateral` transaction.
496
+ */
497
+ async depositCollateral(
498
+ params: LoopscaleDepositCollateralParams,
499
+ ): Promise<LoopscaleVersionedTransactionResponse> {
500
+ const body: Record<string, unknown> = {
501
+ loan: toBase58(params.loan),
502
+ depositMint: toBase58(params.depositMint),
503
+ amount: toSafeNumber(params.amount, "amount"),
504
+ assetType: params.assetType,
505
+ assetIdentifier: toBase58(params.assetIdentifier),
506
+ }
507
+
508
+ if (params.assetIndexGuidance) body.assetIndexGuidance = params.assetIndexGuidance
509
+ if (params.expectedLoanValues) body.expectedLoanValues = params.expectedLoanValues
510
+
511
+ return this.post<LoopscaleVersionedTransactionResponse>(
512
+ "/markets/creditbook/collateral/deposit",
513
+ body,
514
+ withPayerHeader({
515
+ "user-wallet": this.userWallet,
516
+ }, this.payer),
517
+ )
518
+ }
519
+
520
+ /**
521
+ * Build a raw Loopscale `withdrawCollateral` transaction.
522
+ *
523
+ * Full collateral removal can trigger internal loan-maintenance instructions,
524
+ * so callers should accept either a single serialized transaction or a wrapped
525
+ * `transactions` array.
526
+ */
527
+ async withdrawCollateral(
528
+ params: LoopscaleWithdrawCollateralParams,
529
+ ): Promise<LoopscaleLoanTransactionResponse> {
530
+ const body: Record<string, unknown> = {
531
+ loan: toBase58(params.loan),
532
+ collateralMint: toBase58(params.collateralMint),
533
+ amount: toSafeNumber(params.amount, "amount"),
534
+ collateralIndex: params.collateralIndex,
535
+ expectedLoanValues: params.expectedLoanValues,
536
+ }
537
+
538
+ if (params.assetIndexGuidance) body.assetIndexGuidance = params.assetIndexGuidance
539
+
540
+ return this.post<LoopscaleLoanTransactionResponse>(
541
+ "/markets/creditbook/collateral/withdraw",
542
+ body,
543
+ withPayerHeader({
544
+ "user-wallet": this.userWallet,
545
+ }, this.payer),
546
+ )
547
+ }
548
+
549
+ /**
550
+ * Build a raw Loopscale `repay_simple` transaction response.
551
+ */
552
+ async repayLoanSimple(
553
+ params: LoopscaleRepayLoanSimpleParams,
554
+ ): Promise<LoopscaleTransactionsResponse> {
555
+ return this.post<LoopscaleTransactionsResponse>(
556
+ "/markets/creditbook/repay_simple",
557
+ {
558
+ loan: toBase58(params.loan),
559
+ repayParams: {
560
+ amount: toSafeNumber(params.repayParams.amount, "amount"),
561
+ ledgerIndex: params.repayParams.ledgerIndex,
562
+ repayAll: params.repayParams.repayAll,
563
+ },
564
+ strategy: toBase58(params.strategy),
565
+ },
566
+ { payer: this.payer },
567
+ )
568
+ }
569
+
570
+ /**
571
+ * Build a raw Loopscale `close_loan` transaction response.
572
+ */
573
+ async closeLoan(
574
+ params: LoopscaleCloseLoanParams,
575
+ ): Promise<LoopscaleLoanTransactionResponse> {
576
+ return this.post<LoopscaleLoanTransactionResponse>(
577
+ "/markets/creditbook/close_loan",
578
+ { loan: toBase58(params.loan) },
579
+ { "user-wallet": this.userWallet },
580
+ )
581
+ }
582
+
583
+ /**
584
+ * Convert a raw Loopscale response into manager-ready vault transaction pieces.
585
+ *
586
+ * This is the lower-level, appendable "lego" API.
587
+ *
588
+ * Each returned entry represents one manager-facing transaction:
589
+ * send `setupInstructions` first when present, then send the main
590
+ * `instructions`. Wrapped sync transactions are already flattened into one
591
+ * instruction list, so callers do not need to manually inspect the raw
592
+ * Loopscale response to figure out which instructions should be wrapped.
593
+ *
594
+ * Use this when you want to compose Loopscale output with your own custom
595
+ * transaction building logic.
596
+ */
597
+ async prepareVaultTransactions({
598
+ response,
599
+ context,
600
+ }: {
601
+ response: LoopscaleTransactionResponse
602
+ context: LoopscaleVaultPreparationContext
603
+ }): Promise<LoopscalePreparedTransaction[]> {
604
+ return prepareLoopscaleVaultTransactions({ response, context })
605
+ }
606
+
607
+ /**
608
+ * Build locally signed transactions from a raw Loopscale response.
609
+ *
610
+ * This is the simplest manager-facing API.
611
+ *
612
+ * The returned transactions are ready for:
613
+ * 1. `await client.coSign(tx)` when `requiresLoopscaleCoSign === true`
614
+ * 2. `connection.sendTransaction(tx)`
615
+ *
616
+ * The co-signing flag is deliberately per-transaction, not global. A single
617
+ * Loopscale response can produce:
618
+ *
619
+ * - local setup transactions that must be sent as-is,
620
+ * - local top-level transactions that do not belong to Loopscale's signing
621
+ * domain,
622
+ * - one wrapped Loopscale sync transaction that does require Loopscale MPC.
623
+ *
624
+ * The client returns all of them in send order and marks only the wrapped
625
+ * Loopscale transaction for MPC co-signing. This is the correct boundary:
626
+ * callers should not guess which transactions need Loopscale's signature.
627
+ *
628
+ * The first signer in `context.signers` must match `context.signer`, since
629
+ * that signer is used as the fee payer for the compiled transactions.
630
+ */
631
+ async buildVaultTransactions({
632
+ response,
633
+ context,
634
+ }: {
635
+ response: LoopscaleTransactionResponse
636
+ context: LoopscaleBuildTransactionsContext
637
+ }): Promise<LoopscaleBuiltTransaction[]> {
638
+ const feePayer = context.signers[0]
639
+ if (!feePayer) {
640
+ throw new Error("buildVaultTransactions requires at least one signer")
641
+ }
642
+
643
+ if (!feePayer.publicKey.equals(context.signer)) {
644
+ throw new Error("buildVaultTransactions requires signers[0] to match context.signer")
645
+ }
646
+
647
+ const preparedTransactions = await this.prepareVaultTransactions({ response, context })
648
+ const builtTransactions: LoopscaleBuiltTransaction[] = []
649
+
650
+ for (const preparedTransaction of preparedTransactions) {
651
+ if (preparedTransaction.setupInstructions.length > 0) {
652
+ const setupTransaction = await buildSignedTransaction(
653
+ context.connection,
654
+ context.signer,
655
+ preparedTransaction.setupInstructions,
656
+ [],
657
+ context.signers,
658
+ )
659
+
660
+ builtTransactions.push({
661
+ transaction: setupTransaction,
662
+ requiresLoopscaleCoSign: false,
663
+ })
664
+ }
665
+
666
+ const mainTransaction = await buildSignedTransaction(
667
+ context.connection,
668
+ context.signer,
669
+ preparedTransaction.instructions,
670
+ preparedTransaction.addressLookupTableAddresses,
671
+ [...context.signers, ...preparedTransaction.signers],
672
+ )
673
+
674
+ builtTransactions.push({
675
+ transaction: mainTransaction,
676
+ requiresLoopscaleCoSign: preparedTransaction.requiresLoopscaleCoSign,
677
+ })
678
+ }
679
+
680
+ return builtTransactions
681
+ }
682
+
683
+ /**
684
+ * Co-sign a prepared transaction with Loopscale's back-end signer.
685
+ *
686
+ * This uses Loopscale's public MPC co-signing endpoint. The returned
687
+ * transaction is required to keep the exact same message as the locally
688
+ * signed input, and any local signatures that are missing from the response
689
+ * are copied back onto the returned transaction before it is handed to the
690
+ * caller.
691
+ *
692
+ * Only call this for transactions flagged with
693
+ * `requiresLoopscaleCoSign === true`. Transactions not flagged that way do
694
+ * not belong to Loopscale's signing domain and should be sent directly.
695
+ */
696
+ async coSign(transaction: VersionedTransaction): Promise<VersionedTransaction> {
697
+ const serializedTransaction = Buffer.from(transaction.serialize()).toString("base64")
698
+ const data = await this.post<{
699
+ batches: Array<{
700
+ transactions: Array<{
701
+ identifier: string
702
+ transaction: string
703
+ transactionType: number
704
+ }>
705
+ }>
706
+ }>("/mpc/txns/gen", {
707
+ batches: [{
708
+ transactions: [{
709
+ identifier: "loopscale-cosign",
710
+ transaction: serializedTransaction,
711
+ transactionType: 1,
712
+ }],
713
+ }],
714
+ })
715
+
716
+ const transactionEntry = data.batches?.[0]?.transactions?.[0]
717
+ if (!transactionEntry) {
718
+ throw new Error("Loopscale MPC co-signing returned no transaction")
719
+ }
720
+
721
+ const coSignedTransaction = VersionedTransaction.deserialize(
722
+ Buffer.from(transactionEntry.transaction, "base64"),
723
+ )
724
+
725
+ return mergeCoSignedTransaction(transaction, coSignedTransaction)
726
+ }
727
+
728
+ /**
729
+ * Extract the strategy PDA from a `createStrategy` response using the client's connection.
730
+ */
731
+ async extractCreatedStrategyAddress(
732
+ response: LoopscaleTransactionResponse,
733
+ ): Promise<PublicKey> {
734
+ return extractCreatedStrategyAddress(this.connection, response)
735
+ }
736
+
737
+ /**
738
+ * Extract the strategy nonce from a `createStrategy` response using the client's connection.
739
+ */
740
+ async extractCreatedStrategyNonce(
741
+ response: LoopscaleTransactionResponse,
742
+ ): Promise<PublicKey> {
743
+ return extractCreatedStrategyNonce(this.connection, response)
744
+ }
745
+
746
+ private async post<T>(
747
+ path: string,
748
+ body?: unknown,
749
+ extraHeaders?: Record<string, string>,
750
+ ): Promise<T> {
751
+ const url = `${this.baseUrl}${path}`
752
+ const headers: Record<string, string> = {
753
+ "Content-Type": "application/json",
754
+ ...extraHeaders,
755
+ }
756
+
757
+ if (this.debug) {
758
+ console.log(`\n[LoopscaleClient] POST ${url}`)
759
+ if (body !== undefined) {
760
+ console.log(` body: ${JSON.stringify(body, null, 2).split("\n").slice(0, 10).join("\n")}...`)
761
+ }
762
+ }
763
+
764
+ let response: Response | undefined
765
+ const maxRetries = 3
766
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
767
+ response = await fetch(url, {
768
+ method: "POST",
769
+ headers,
770
+ body: body !== undefined ? JSON.stringify(body) : undefined,
771
+ })
772
+
773
+ if (response.status === 429 && attempt < maxRetries) {
774
+ const delayMs = 2_000 * (attempt + 1)
775
+ if (this.debug) {
776
+ console.log(` RATE LIMITED (429), retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries})...`)
777
+ }
778
+ await new Promise((resolve) => setTimeout(resolve, delayMs))
779
+ continue
780
+ }
781
+
782
+ break
783
+ }
784
+
785
+ if (!response?.ok) {
786
+ const text = await response?.text()
787
+ if (this.debug) {
788
+ console.log(` ERROR ${response?.status}: ${text}`)
789
+ }
790
+ throw new Error(`Loopscale API ${path} failed (${response?.status}): ${text}`)
791
+ }
792
+
793
+ const data = await response.json() as T
794
+ if (this.debug) {
795
+ console.log(` OK ${response.status}`)
796
+ }
797
+ return data
798
+ }
799
+ }
800
+
801
+ export {
802
+ deserializeLoopscaleTransactionBatchResponse,
803
+ deserializeLoopscaleTransactionResponse,
804
+ extractCreatedStrategyAddress,
805
+ extractCreatedStrategyNonce,
806
+ getLoopscaleTransactionResponses,
807
+ identifyLoopscaleInstruction,
808
+ }