@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.
- package/build/client/vaults/index.d.ts +2 -0
- package/build/client/vaults/index.js +2 -0
- package/build/client/vaults/index.js.map +1 -1
- package/build/client/vaults/types/index.d.ts +2 -0
- package/build/client/vaults/types/index.js +2 -0
- package/build/client/vaults/types/index.js.map +1 -1
- package/build/client/vaults/types/kaminoFarmEntry.d.ts +15 -0
- package/build/client/vaults/types/kaminoFarmEntry.js +17 -0
- package/build/client/vaults/types/kaminoFarmEntry.js.map +1 -0
- package/build/client/vaults/types/kaminoObligationEntry.d.ts +21 -4
- package/build/client/vaults/types/kaminoObligationEntry.js +2 -1
- package/build/client/vaults/types/kaminoObligationEntry.js.map +1 -1
- package/build/client/vaults/types/positionUpdate.d.ts +9 -0
- package/build/client/vaults/types/positionUpdate.js +23 -0
- package/build/client/vaults/types/positionUpdate.js.map +1 -1
- package/build/client/vaults/types/proposalAction.js +0 -3
- package/build/client/vaults/types/proposalAction.js.map +1 -1
- package/build/client/vaults/types/reserveFarmMapping.d.ts +19 -0
- package/build/client/vaults/types/reserveFarmMapping.js +18 -0
- package/build/client/vaults/types/reserveFarmMapping.js.map +1 -0
- package/build/client/vaults/types/strategyPosition.d.ts +5 -0
- package/build/client/vaults/types/strategyPosition.js +5 -0
- package/build/client/vaults/types/strategyPosition.js.map +1 -1
- package/build/exponentVaults/aumCalculator.d.ts +25 -4
- package/build/exponentVaults/aumCalculator.js +236 -15
- package/build/exponentVaults/aumCalculator.js.map +1 -1
- package/build/exponentVaults/fetcher.d.ts +52 -0
- package/build/exponentVaults/fetcher.js +199 -0
- package/build/exponentVaults/fetcher.js.map +1 -0
- package/build/exponentVaults/index.d.ts +10 -9
- package/build/exponentVaults/index.js +26 -8
- package/build/exponentVaults/index.js.map +1 -1
- package/build/exponentVaults/kamino-farms.d.ts +144 -0
- package/build/exponentVaults/kamino-farms.js +396 -0
- package/build/exponentVaults/kamino-farms.js.map +1 -0
- package/build/exponentVaults/loopscale/client.d.ts +240 -0
- package/build/exponentVaults/loopscale/client.js +590 -0
- package/build/exponentVaults/loopscale/client.js.map +1 -0
- package/build/exponentVaults/loopscale/client.test.d.ts +1 -0
- package/build/exponentVaults/loopscale/client.test.js +183 -0
- package/build/exponentVaults/loopscale/client.test.js.map +1 -0
- package/build/exponentVaults/loopscale/helpers.d.ts +29 -0
- package/build/exponentVaults/loopscale/helpers.js +119 -0
- package/build/exponentVaults/loopscale/helpers.js.map +1 -0
- package/build/exponentVaults/loopscale/index.d.ts +3 -0
- package/build/exponentVaults/loopscale/index.js +12 -0
- package/build/exponentVaults/loopscale/index.js.map +1 -0
- package/build/exponentVaults/loopscale/prepared-transactions.d.ts +13 -0
- package/build/exponentVaults/loopscale/prepared-transactions.js +271 -0
- package/build/exponentVaults/loopscale/prepared-transactions.js.map +1 -0
- package/build/exponentVaults/loopscale/prepared-transactions.test.d.ts +1 -0
- package/build/exponentVaults/loopscale/prepared-transactions.test.js +400 -0
- package/build/exponentVaults/loopscale/prepared-transactions.test.js.map +1 -0
- package/build/exponentVaults/loopscale/prepared-types.d.ts +62 -0
- package/build/exponentVaults/loopscale/prepared-types.js +3 -0
- package/build/exponentVaults/loopscale/prepared-types.js.map +1 -0
- package/build/exponentVaults/loopscale/response-plan.d.ts +69 -0
- package/build/exponentVaults/loopscale/response-plan.js +141 -0
- package/build/exponentVaults/loopscale/response-plan.js.map +1 -0
- package/build/exponentVaults/loopscale/response-plan.test.d.ts +1 -0
- package/build/exponentVaults/loopscale/response-plan.test.js +139 -0
- package/build/exponentVaults/loopscale/response-plan.test.js.map +1 -0
- package/build/exponentVaults/loopscale/send-plan.d.ts +75 -0
- package/build/exponentVaults/loopscale/send-plan.js +235 -0
- package/build/exponentVaults/loopscale/send-plan.js.map +1 -0
- package/build/exponentVaults/loopscale/types.d.ts +443 -0
- package/build/exponentVaults/loopscale/types.js +3 -0
- package/build/exponentVaults/loopscale/types.js.map +1 -0
- package/build/exponentVaults/loopscale-client.d.ts +113 -524
- package/build/exponentVaults/loopscale-client.js +296 -539
- package/build/exponentVaults/loopscale-client.js.map +1 -1
- package/build/exponentVaults/loopscale-client.test.d.ts +1 -0
- package/build/exponentVaults/loopscale-client.test.js +162 -0
- package/build/exponentVaults/loopscale-client.test.js.map +1 -0
- package/build/exponentVaults/loopscale-client.types.d.ts +425 -0
- package/build/exponentVaults/loopscale-client.types.js +3 -0
- package/build/exponentVaults/loopscale-client.types.js.map +1 -0
- package/build/exponentVaults/loopscale-execution.d.ts +125 -0
- package/build/exponentVaults/loopscale-execution.js +341 -0
- package/build/exponentVaults/loopscale-execution.js.map +1 -0
- package/build/exponentVaults/loopscale-execution.test.d.ts +1 -0
- package/build/exponentVaults/loopscale-execution.test.js +139 -0
- package/build/exponentVaults/loopscale-execution.test.js.map +1 -0
- package/build/exponentVaults/loopscale-vault.d.ts +115 -0
- package/build/exponentVaults/loopscale-vault.js +275 -0
- package/build/exponentVaults/loopscale-vault.js.map +1 -0
- package/build/exponentVaults/loopscale-vault.test.d.ts +1 -0
- package/build/exponentVaults/loopscale-vault.test.js +102 -0
- package/build/exponentVaults/loopscale-vault.test.js.map +1 -0
- package/build/exponentVaults/policyBuilders.d.ts +62 -0
- package/build/exponentVaults/policyBuilders.js +119 -2
- package/build/exponentVaults/policyBuilders.js.map +1 -1
- package/build/exponentVaults/pricePathResolver.d.ts +45 -0
- package/build/exponentVaults/pricePathResolver.js +198 -0
- package/build/exponentVaults/pricePathResolver.js.map +1 -0
- package/build/exponentVaults/pricePathResolver.test.d.ts +1 -0
- package/build/exponentVaults/pricePathResolver.test.js +369 -0
- package/build/exponentVaults/pricePathResolver.test.js.map +1 -0
- package/build/exponentVaults/syncTransaction.js +4 -1
- package/build/exponentVaults/syncTransaction.js.map +1 -1
- package/build/exponentVaults/titan-quote.js +170 -36
- package/build/exponentVaults/titan-quote.js.map +1 -1
- package/build/exponentVaults/vault-instruction-types.d.ts +363 -0
- package/build/exponentVaults/vault-instruction-types.js +128 -0
- package/build/exponentVaults/vault-instruction-types.js.map +1 -0
- package/build/exponentVaults/vault-interaction.d.ts +203 -343
- package/build/exponentVaults/vault-interaction.js +1894 -426
- package/build/exponentVaults/vault-interaction.js.map +1 -1
- package/build/exponentVaults/vault-interaction.kamino-vault.test.d.ts +1 -0
- package/build/exponentVaults/vault-interaction.kamino-vault.test.js +143 -0
- package/build/exponentVaults/vault-interaction.kamino-vault.test.js.map +1 -0
- package/build/exponentVaults/vault.d.ts +51 -2
- package/build/exponentVaults/vault.js +324 -48
- package/build/exponentVaults/vault.js.map +1 -1
- package/build/exponentVaults/vaultTransactionBuilder.d.ts +100 -134
- package/build/exponentVaults/vaultTransactionBuilder.js +383 -285
- package/build/exponentVaults/vaultTransactionBuilder.js.map +1 -1
- package/build/exponentVaults/vaultTransactionBuilder.test.d.ts +1 -0
- package/build/exponentVaults/vaultTransactionBuilder.test.js +297 -0
- package/build/exponentVaults/vaultTransactionBuilder.test.js.map +1 -0
- package/build/marketThree.d.ts +6 -2
- package/build/marketThree.js +10 -8
- package/build/marketThree.js.map +1 -1
- package/package.json +34 -32
- package/src/client/vaults/index.ts +2 -0
- package/src/client/vaults/types/index.ts +2 -0
- package/src/client/vaults/types/kaminoFarmEntry.ts +32 -0
- package/src/client/vaults/types/kaminoObligationEntry.ts +6 -3
- package/src/client/vaults/types/positionUpdate.ts +62 -0
- package/src/client/vaults/types/proposalAction.ts +0 -3
- package/src/client/vaults/types/reserveFarmMapping.ts +35 -0
- package/src/client/vaults/types/strategyPosition.ts +18 -1
- package/src/exponentVaults/aumCalculator.ts +353 -16
- package/src/exponentVaults/fetcher.ts +257 -0
- package/src/exponentVaults/index.ts +65 -40
- package/src/exponentVaults/kamino-farms.ts +538 -0
- package/src/exponentVaults/loopscale/client.ts +808 -0
- package/src/exponentVaults/loopscale/helpers.ts +172 -0
- package/src/exponentVaults/loopscale/index.ts +57 -0
- package/src/exponentVaults/loopscale/prepared-transactions.ts +435 -0
- package/src/exponentVaults/loopscale/prepared-types.ts +73 -0
- package/src/exponentVaults/loopscale/types.ts +466 -0
- package/src/exponentVaults/policyBuilders.ts +170 -0
- package/src/exponentVaults/pricePathResolver.test.ts +466 -0
- package/src/exponentVaults/pricePathResolver.ts +273 -0
- package/src/exponentVaults/syncTransaction.ts +6 -1
- package/src/exponentVaults/titan-quote.ts +231 -45
- package/src/exponentVaults/vault-instruction-types.ts +493 -0
- package/src/exponentVaults/vault-interaction.kamino-vault.test.ts +149 -0
- package/src/exponentVaults/vault-interaction.ts +2818 -799
- package/src/exponentVaults/vault.ts +474 -63
- package/src/exponentVaults/vaultTransactionBuilder.test.ts +349 -0
- package/src/exponentVaults/vaultTransactionBuilder.ts +581 -433
- package/src/marketThree.ts +14 -6
- 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
|
+
}
|