@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
|
@@ -1,39 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LoopscaleClient = void 0;
|
|
3
|
+
exports.LoopscaleClient = exports.extractCreatedStrategyNonce = exports.extractCreatedStrategyAddress = exports.getLoopscaleTransactionResponses = exports.deserializeLoopscaleTransactionBatchResponse = exports.deserializeLoopscaleTransactionResponse = exports.identifyLoopscaleInstruction = void 0;
|
|
4
4
|
const web3_js_1 = require("@solana/web3.js");
|
|
5
5
|
const policyBuilders_1 = require("./policyBuilders");
|
|
6
|
-
const vault_interaction_1 = require("./vault-interaction");
|
|
7
|
-
// ============================================================================
|
|
8
|
-
// Constants
|
|
9
|
-
// ============================================================================
|
|
10
6
|
const LOOPSCALE_API_BASE_URL = "https://tars.loopscale.com/v1";
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const DISCRIMINATOR_HEX_TO_NAME = new Map(Object.entries(policyBuilders_1.LOOPSCALE_DISCRIMINATORS).map(([name, buf]) => [
|
|
14
|
-
Buffer.from(buf).toString("hex"),
|
|
7
|
+
const DISCRIMINATOR_HEX_TO_NAME = new Map(Object.entries(policyBuilders_1.LOOPSCALE_DISCRIMINATORS).map(([name, discriminator]) => [
|
|
8
|
+
Buffer.from(discriminator).toString("hex"),
|
|
15
9
|
name,
|
|
16
10
|
]));
|
|
17
|
-
const NAME_TO_ACTION_WRAPPER = {
|
|
18
|
-
createLoan: (ix) => vault_interaction_1.loopscaleAction.createLoan({ instruction: ix }),
|
|
19
|
-
depositCollateral: (ix) => vault_interaction_1.loopscaleAction.depositCollateral({ instruction: ix }),
|
|
20
|
-
borrowPrincipal: (ix) => vault_interaction_1.loopscaleAction.borrowPrincipal({ instruction: ix }),
|
|
21
|
-
repayPrincipal: (ix) => vault_interaction_1.loopscaleAction.repayPrincipal({ instruction: ix }),
|
|
22
|
-
withdrawCollateral: (ix) => vault_interaction_1.loopscaleAction.withdrawCollateral({ instruction: ix }),
|
|
23
|
-
closeLoan: (ix) => vault_interaction_1.loopscaleAction.closeLoan({ instruction: ix }),
|
|
24
|
-
updateWeightMatrix: (ix) => vault_interaction_1.loopscaleAction.updateWeightMatrix({ instruction: ix }),
|
|
25
|
-
createStrategy: (ix) => vault_interaction_1.loopscaleAction.createStrategy({ instruction: ix }),
|
|
26
|
-
depositStrategy: (ix) => vault_interaction_1.loopscaleAction.depositStrategy({ instruction: ix }),
|
|
27
|
-
withdrawStrategy: (ix) => vault_interaction_1.loopscaleAction.withdrawStrategy({ instruction: ix }),
|
|
28
|
-
closeStrategy: (ix) => vault_interaction_1.loopscaleAction.closeStrategy({ instruction: ix }),
|
|
29
|
-
updateStrategy: (ix) => vault_interaction_1.loopscaleAction.updateStrategy({ instruction: ix }),
|
|
30
|
-
lockLoan: (ix) => vault_interaction_1.loopscaleAction.lockLoan({ instruction: ix }),
|
|
31
|
-
unlockLoan: (ix) => vault_interaction_1.loopscaleAction.unlockLoan({ instruction: ix }),
|
|
32
|
-
refinanceLedger: (ix) => vault_interaction_1.loopscaleAction.refinanceLedger({ instruction: ix }),
|
|
33
|
-
};
|
|
34
|
-
// ============================================================================
|
|
35
|
-
// Internal helpers
|
|
36
|
-
// ============================================================================
|
|
37
11
|
function toBase58(value) {
|
|
38
12
|
return typeof value === "string" ? value : value.toBase58();
|
|
39
13
|
}
|
|
@@ -42,211 +16,181 @@ function toSafeNumber(value, field) {
|
|
|
42
16
|
if (value < 0n)
|
|
43
17
|
throw new Error(`${field} must be non-negative`);
|
|
44
18
|
if (value > BigInt(Number.MAX_SAFE_INTEGER))
|
|
45
|
-
throw new Error(`${field} too large for JSON
|
|
19
|
+
throw new Error(`${field} is too large for a JSON number`);
|
|
46
20
|
return Number(value);
|
|
47
21
|
}
|
|
48
|
-
if (!Number.isInteger(value) || value < 0)
|
|
22
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
49
23
|
throw new Error(`${field} must be a non-negative integer`);
|
|
24
|
+
}
|
|
50
25
|
return value;
|
|
51
26
|
}
|
|
52
|
-
function
|
|
53
|
-
return
|
|
27
|
+
function hasSignature(signature) {
|
|
28
|
+
return signature.some((value) => value !== 0);
|
|
54
29
|
}
|
|
55
|
-
function
|
|
56
|
-
if (!
|
|
57
|
-
return
|
|
58
|
-
|
|
59
|
-
|
|
30
|
+
function withPayerHeader(headers, payer) {
|
|
31
|
+
if (!payer)
|
|
32
|
+
return headers;
|
|
33
|
+
return { ...headers, payer };
|
|
34
|
+
}
|
|
35
|
+
function buildTransactionFromResponse(response) {
|
|
36
|
+
const message = web3_js_1.VersionedMessage.deserialize(Buffer.from(response.message, "base64"));
|
|
37
|
+
const transaction = new web3_js_1.VersionedTransaction(message);
|
|
38
|
+
const signerKeys = message.staticAccountKeys.slice(0, message.header.numRequiredSignatures);
|
|
39
|
+
for (const signature of response.signatures) {
|
|
40
|
+
const signer = new web3_js_1.PublicKey(signature.publicKey);
|
|
41
|
+
const bytes = Buffer.from(signature.signature, "base64");
|
|
42
|
+
if (bytes.length !== 64) {
|
|
43
|
+
throw new Error(`Invalid signature for ${signature.publicKey}`);
|
|
44
|
+
}
|
|
45
|
+
const signerIndex = signerKeys.findIndex((candidate) => candidate.equals(signer));
|
|
46
|
+
if (signerIndex >= 0) {
|
|
47
|
+
transaction.signatures[signerIndex] = new Uint8Array(bytes);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return transaction;
|
|
51
|
+
}
|
|
52
|
+
function mergeCoSignedTransaction(transaction, coSignedTransaction) {
|
|
53
|
+
const left = Buffer.from(transaction.message.serialize());
|
|
54
|
+
const right = Buffer.from(coSignedTransaction.message.serialize());
|
|
55
|
+
if (!left.equals(right)) {
|
|
56
|
+
throw new Error("Loopscale returned a different transaction message; refusing to merge signatures");
|
|
57
|
+
}
|
|
58
|
+
const signerKeys = coSignedTransaction.message.staticAccountKeys
|
|
59
|
+
.slice(0, coSignedTransaction.message.header.numRequiredSignatures);
|
|
60
|
+
const inputSignerKeys = transaction.message.staticAccountKeys
|
|
61
|
+
.slice(0, transaction.message.header.numRequiredSignatures);
|
|
62
|
+
for (let index = 0; index < inputSignerKeys.length; index += 1) {
|
|
63
|
+
const inputSignature = transaction.signatures[index];
|
|
64
|
+
if (!hasSignature(inputSignature))
|
|
65
|
+
continue;
|
|
66
|
+
const signerIndex = signerKeys.findIndex((candidate) => candidate.equals(inputSignerKeys[index]));
|
|
67
|
+
if (signerIndex >= 0 && !hasSignature(coSignedTransaction.signatures[signerIndex])) {
|
|
68
|
+
coSignedTransaction.signatures[signerIndex] = inputSignature;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return coSignedTransaction;
|
|
60
72
|
}
|
|
61
|
-
async function
|
|
62
|
-
|
|
73
|
+
async function resolveAddressLookupTables(connection, transaction) {
|
|
74
|
+
return Promise.all(transaction.message.addressTableLookups.map(async (lookup) => {
|
|
63
75
|
const result = await connection.getAddressLookupTable(lookup.accountKey);
|
|
64
76
|
if (!result.value) {
|
|
65
77
|
throw new Error(`Missing lookup table ${lookup.accountKey.toBase58()}`);
|
|
66
78
|
}
|
|
67
79
|
return result.value;
|
|
68
80
|
}));
|
|
69
|
-
return web3_js_1.TransactionMessage.decompile(transaction.message, { addressLookupTableAccounts: lookupTableAccounts }).instructions;
|
|
70
81
|
}
|
|
71
|
-
function
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
throw new Error(`Invalid signature for ${pubkey.toBase58()}`);
|
|
80
|
-
const idx = signerKeys.findIndex((k) => k.equals(pubkey));
|
|
81
|
-
if (idx >= 0)
|
|
82
|
-
tx.signatures[idx] = new Uint8Array(sigBytes);
|
|
82
|
+
async function findInstructionByName(connection, transactions, name) {
|
|
83
|
+
for (const transaction of transactions) {
|
|
84
|
+
const addressLookupTableAccounts = await resolveAddressLookupTables(connection, transaction);
|
|
85
|
+
const instructions = web3_js_1.TransactionMessage.decompile(transaction.message, { addressLookupTableAccounts }).instructions;
|
|
86
|
+
const match = instructions.find((instruction) => identifyLoopscaleInstruction(instruction) === name);
|
|
87
|
+
if (match) {
|
|
88
|
+
return match;
|
|
89
|
+
}
|
|
83
90
|
}
|
|
84
|
-
return
|
|
91
|
+
return null;
|
|
85
92
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (resultIdx < 0)
|
|
96
|
-
continue;
|
|
97
|
-
if (isEmptySignature(resultTx.signatures[resultIdx])) {
|
|
98
|
-
resultTx.signatures[resultIdx] = inputSig;
|
|
99
|
-
}
|
|
93
|
+
/**
|
|
94
|
+
* Identify a Loopscale strategy or loan instruction from its discriminator.
|
|
95
|
+
*
|
|
96
|
+
* Returns `null` for non-Loopscale instructions and for unsupported Loopscale
|
|
97
|
+
* instructions outside the strategies + loans scope of this SDK module.
|
|
98
|
+
*/
|
|
99
|
+
function identifyLoopscaleInstruction(instruction) {
|
|
100
|
+
if (!instruction.programId.equals(policyBuilders_1.LOOPSCALE_PROGRAM_ID)) {
|
|
101
|
+
return null;
|
|
100
102
|
}
|
|
101
|
-
|
|
103
|
+
const discriminator = Buffer.from(instruction.data).subarray(0, 8).toString("hex");
|
|
104
|
+
return DISCRIMINATOR_HEX_TO_NAME.get(discriminator) ?? null;
|
|
105
|
+
}
|
|
106
|
+
exports.identifyLoopscaleInstruction = identifyLoopscaleInstruction;
|
|
107
|
+
/**
|
|
108
|
+
* Deserialize a raw Loopscale transaction response into a `VersionedTransaction`.
|
|
109
|
+
*/
|
|
110
|
+
function deserializeLoopscaleTransactionResponse(response) {
|
|
111
|
+
return buildTransactionFromResponse(response);
|
|
112
|
+
}
|
|
113
|
+
exports.deserializeLoopscaleTransactionResponse = deserializeLoopscaleTransactionResponse;
|
|
114
|
+
/**
|
|
115
|
+
* Deserialize a raw Loopscale transaction batch response into `VersionedTransaction`s.
|
|
116
|
+
*/
|
|
117
|
+
function deserializeLoopscaleTransactionBatchResponse(response) {
|
|
118
|
+
return response.map((entry) => deserializeLoopscaleTransactionResponse(entry));
|
|
102
119
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
120
|
+
exports.deserializeLoopscaleTransactionBatchResponse = deserializeLoopscaleTransactionBatchResponse;
|
|
121
|
+
/**
|
|
122
|
+
* Extract the raw transaction responses from any supported Loopscale transaction response shape.
|
|
123
|
+
*/
|
|
124
|
+
function getLoopscaleTransactionResponses(response) {
|
|
125
|
+
if (Array.isArray(response)) {
|
|
126
|
+
return response;
|
|
127
|
+
}
|
|
128
|
+
if ("transactions" in response) {
|
|
129
|
+
return response.transactions;
|
|
130
|
+
}
|
|
131
|
+
if ("transaction" in response) {
|
|
132
|
+
return [response.transaction];
|
|
108
133
|
}
|
|
134
|
+
return [response];
|
|
109
135
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
136
|
+
exports.getLoopscaleTransactionResponses = getLoopscaleTransactionResponses;
|
|
137
|
+
/**
|
|
138
|
+
* Extract the strategy PDA created by a Loopscale `createStrategy` response.
|
|
139
|
+
*/
|
|
140
|
+
async function extractCreatedStrategyAddress(connection, response) {
|
|
141
|
+
const transactions = getLoopscaleTransactionResponses(response)
|
|
142
|
+
.map((entry) => deserializeLoopscaleTransactionResponse(entry));
|
|
143
|
+
const instruction = await findInstructionByName(connection, transactions, "createStrategy");
|
|
144
|
+
const strategy = instruction?.keys[3]?.pubkey;
|
|
145
|
+
if (!strategy) {
|
|
146
|
+
throw new Error("Loopscale createStrategy response did not contain a createStrategy instruction");
|
|
147
|
+
}
|
|
148
|
+
return strategy;
|
|
149
|
+
}
|
|
150
|
+
exports.extractCreatedStrategyAddress = extractCreatedStrategyAddress;
|
|
151
|
+
/**
|
|
152
|
+
* Extract the nonce public key used by a Loopscale `createStrategy` response.
|
|
153
|
+
*/
|
|
154
|
+
async function extractCreatedStrategyNonce(connection, response) {
|
|
155
|
+
const transactions = getLoopscaleTransactionResponses(response)
|
|
156
|
+
.map((entry) => deserializeLoopscaleTransactionResponse(entry));
|
|
157
|
+
const instruction = await findInstructionByName(connection, transactions, "createStrategy");
|
|
158
|
+
const nonce = instruction?.keys[2]?.pubkey;
|
|
159
|
+
if (!nonce) {
|
|
160
|
+
throw new Error("Loopscale createStrategy response did not contain the strategy nonce account");
|
|
161
|
+
}
|
|
162
|
+
return nonce;
|
|
163
|
+
}
|
|
164
|
+
exports.extractCreatedStrategyNonce = extractCreatedStrategyNonce;
|
|
165
|
+
/**
|
|
166
|
+
* Thin docs-aligned Loopscale client for strategies, loans, and related query endpoints.
|
|
167
|
+
*
|
|
168
|
+
* Transaction methods intentionally return the raw Loopscale response shapes
|
|
169
|
+
* instead of derived sync-action bundles. Use the execution helpers from
|
|
170
|
+
* `loopscale-execution.ts` when you need to turn a raw response into Exponent
|
|
171
|
+
* transaction steps.
|
|
172
|
+
*
|
|
173
|
+
* Loan-lock endpoints such as repay, close, and refinance are intentionally
|
|
174
|
+
* excluded from this client. Loopscale documents those responses separately,
|
|
175
|
+
* but the returned lock/unlock flows are not compatible with the Exponent CPI
|
|
176
|
+
* execution path this SDK is designed around.
|
|
177
|
+
*/
|
|
113
178
|
class LoopscaleClient {
|
|
114
179
|
connection;
|
|
115
180
|
baseUrl;
|
|
116
181
|
userWallet;
|
|
117
|
-
|
|
118
|
-
useMpcCoSign;
|
|
182
|
+
payer;
|
|
119
183
|
debug;
|
|
120
|
-
vaultCtx = null;
|
|
121
184
|
constructor(config) {
|
|
122
185
|
this.connection = config.connection;
|
|
123
186
|
this.baseUrl = config.baseUrl ?? LOOPSCALE_API_BASE_URL;
|
|
124
187
|
this.userWallet = toBase58(config.userWallet);
|
|
125
|
-
this.
|
|
126
|
-
this.useMpcCoSign = config.useMpcCoSign ?? true;
|
|
188
|
+
this.payer = toBase58(config.payer ?? config.userWallet);
|
|
127
189
|
this.debug = config.debug ?? false;
|
|
128
190
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
requireVaultContext() {
|
|
134
|
-
if (!this.vaultCtx) {
|
|
135
|
-
throw new Error("LoopscaleClient: call setVaultContext() before executeBatches/executeSyncTx");
|
|
136
|
-
}
|
|
137
|
-
return this.vaultCtx;
|
|
138
|
-
}
|
|
139
|
-
async executeBatches(batches) {
|
|
140
|
-
const ctx = this.requireVaultContext();
|
|
141
|
-
const commitment = ctx.commitment ?? "confirmed";
|
|
142
|
-
const results = [];
|
|
143
|
-
for (const [batchIndex, batch] of batches.entries()) {
|
|
144
|
-
const result = batch.syncActions.length === 0
|
|
145
|
-
? await this.executePrebuiltBatch(batch, batchIndex, commitment)
|
|
146
|
-
: await this.executeSyncBatch(batch, batchIndex, ctx, commitment);
|
|
147
|
-
results.push(result);
|
|
148
|
-
if (result.error) {
|
|
149
|
-
return { error: result.error, results };
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return { error: null, results };
|
|
153
|
-
}
|
|
154
|
-
async executeSyncTx(syncActions) {
|
|
155
|
-
const ctx = this.requireVaultContext();
|
|
156
|
-
const commitment = ctx.commitment ?? "confirmed";
|
|
157
|
-
return this.executeSyncBatch({ syncActions, topLevelInstructions: [], setupInstructions: [] }, 0, ctx, commitment);
|
|
158
|
-
}
|
|
159
|
-
async executePrebuiltBatch(batch, batchIndex, commitment) {
|
|
160
|
-
const ctx = this.requireVaultContext();
|
|
161
|
-
const tx = batch.raw.transaction;
|
|
162
|
-
const signerKeys = tx.message.staticAccountKeys.slice(0, tx.message.header.numRequiredSignatures);
|
|
163
|
-
const available = new Map(ctx.signers.map((s) => [s.publicKey.toBase58(), s]));
|
|
164
|
-
const local = signerKeys.map((k) => available.get(k.toBase58())).filter((s) => Boolean(s));
|
|
165
|
-
if (local.length > 0)
|
|
166
|
-
tx.sign(local);
|
|
167
|
-
const missing = signerKeys.filter((_, i) => tx.signatures[i]?.every((b) => b === 0));
|
|
168
|
-
if (missing.length > 0) {
|
|
169
|
-
throw new Error(`batch ${batchIndex + 1}: missing signer(s): ${missing.map((k) => k.toBase58()).join(", ")}`);
|
|
170
|
-
}
|
|
171
|
-
return { batchIndex, ...(await this.sendAndConfirm(tx, ctx.sendOptions, { commitment })) };
|
|
172
|
-
}
|
|
173
|
-
async executeSyncBatch(batch, batchIndex, ctx, commitment) {
|
|
174
|
-
const syncResult = await (0, vault_interaction_1.createVaultSyncTransaction)({
|
|
175
|
-
instructions: batch.syncActions,
|
|
176
|
-
owner: ctx.owner,
|
|
177
|
-
connection: this.connection,
|
|
178
|
-
policyPda: ctx.policyPda,
|
|
179
|
-
vaultPda: ctx.vaultPda,
|
|
180
|
-
signer: ctx.signer,
|
|
181
|
-
accountIndex: ctx.accountIndex,
|
|
182
|
-
constraintIndices: ctx.constraintIndices,
|
|
183
|
-
vaultAddress: ctx.vaultAddress,
|
|
184
|
-
leadingAccounts: ctx.leadingAccounts,
|
|
185
|
-
preHookAccounts: ctx.preHookAccounts,
|
|
186
|
-
postHookAccounts: ctx.postHookAccounts,
|
|
187
|
-
squadsProgram: ctx.squadsProgram,
|
|
188
|
-
});
|
|
189
|
-
if (syncResult.setupInstructions.length > 0) {
|
|
190
|
-
throw new Error(`batch ${batchIndex + 1}: unexpected sync setupInstructions (${syncResult.setupInstructions.length})`);
|
|
191
|
-
}
|
|
192
|
-
const { blockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash(commitment);
|
|
193
|
-
const msg = new web3_js_1.TransactionMessage({
|
|
194
|
-
payerKey: ctx.signers[0].publicKey,
|
|
195
|
-
recentBlockhash: blockhash,
|
|
196
|
-
instructions: [
|
|
197
|
-
...(ctx.prependInstructions ?? []),
|
|
198
|
-
...batch.topLevelInstructions,
|
|
199
|
-
...syncResult.preInstructions,
|
|
200
|
-
syncResult.instruction,
|
|
201
|
-
...syncResult.postInstructions,
|
|
202
|
-
],
|
|
203
|
-
}).compileToV0Message(ctx.addressLookupTableAccounts ?? []);
|
|
204
|
-
let tx = new web3_js_1.VersionedTransaction(msg);
|
|
205
|
-
tx.sign([...ctx.signers, ...syncResult.signers]);
|
|
206
|
-
tx = await this.coSign(tx);
|
|
207
|
-
return {
|
|
208
|
-
batchIndex,
|
|
209
|
-
...(await this.sendAndConfirm(tx, ctx.sendOptions, { commitment, blockhash, lastValidBlockHeight })),
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
async sendAndConfirm(tx, sendOptions, confirmation) {
|
|
213
|
-
const signature = await this.connection.sendTransaction(tx, sendOptions);
|
|
214
|
-
if (confirmation.blockhash && confirmation.lastValidBlockHeight !== undefined) {
|
|
215
|
-
try {
|
|
216
|
-
await this.connection.confirmTransaction({
|
|
217
|
-
signature,
|
|
218
|
-
blockhash: confirmation.blockhash,
|
|
219
|
-
lastValidBlockHeight: confirmation.lastValidBlockHeight,
|
|
220
|
-
}, confirmation.commitment);
|
|
221
|
-
}
|
|
222
|
-
catch (err) {
|
|
223
|
-
if (this.debug)
|
|
224
|
-
console.warn(`[LoopscaleClient] confirmTransaction failed for ${signature}:`, err);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
else {
|
|
228
|
-
for (let attempt = 0; attempt < 60; attempt += 1) {
|
|
229
|
-
const statuses = await this.connection.getSignatureStatuses([signature]);
|
|
230
|
-
const s = statuses.value[0];
|
|
231
|
-
if (s?.err || s?.confirmationStatus === "confirmed" || s?.confirmationStatus === "finalized")
|
|
232
|
-
break;
|
|
233
|
-
await new Promise((r) => setTimeout(r, 1_000));
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
const finality = confirmation.commitment === "finalized" ? "finalized" : "confirmed";
|
|
237
|
-
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
238
|
-
const txResult = await this.connection.getTransaction(signature, {
|
|
239
|
-
commitment: finality,
|
|
240
|
-
maxSupportedTransactionVersion: 0,
|
|
241
|
-
});
|
|
242
|
-
if (txResult) {
|
|
243
|
-
return { signature, logs: txResult.meta?.logMessages ?? null, error: txResult.meta?.err ?? null };
|
|
244
|
-
}
|
|
245
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
246
|
-
}
|
|
247
|
-
return { signature, logs: null, error: `Transaction ${signature} not found after confirmation timeout` };
|
|
248
|
-
}
|
|
249
|
-
// ── Data ──
|
|
191
|
+
/**
|
|
192
|
+
* Load Loopscale quotes for a principal/collateral pair.
|
|
193
|
+
*/
|
|
250
194
|
async getQuotes(params) {
|
|
251
195
|
const body = {
|
|
252
196
|
durationType: params.durationType,
|
|
@@ -256,47 +200,48 @@ class LoopscaleClient {
|
|
|
256
200
|
limit: params.limit ?? 10,
|
|
257
201
|
offset: params.offset ?? 0,
|
|
258
202
|
};
|
|
259
|
-
|
|
203
|
+
if (params.minPrincipalAmount !== undefined) {
|
|
204
|
+
body.minPrincipalAmount = toSafeNumber(params.minPrincipalAmount, "minPrincipalAmount");
|
|
205
|
+
}
|
|
206
|
+
return this.post("/markets/quote", body, {
|
|
207
|
+
"user-wallet": this.userWallet,
|
|
208
|
+
});
|
|
260
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Load the best quote for one or more specific collateral assets.
|
|
212
|
+
*/
|
|
261
213
|
async getMaxQuote(params) {
|
|
262
|
-
|
|
214
|
+
return this.post("/markets/quote/max", {
|
|
263
215
|
durationType: params.durationType,
|
|
264
216
|
duration: params.duration,
|
|
265
217
|
principalMint: toBase58(params.principalMint),
|
|
266
|
-
collateralFilter: params.collateralFilter.map((
|
|
267
|
-
amount: toSafeNumber(
|
|
268
|
-
assetData:
|
|
218
|
+
collateralFilter: params.collateralFilter.map((item) => ({
|
|
219
|
+
amount: toSafeNumber(item.amount, "amount"),
|
|
220
|
+
assetData: item.assetData,
|
|
269
221
|
})),
|
|
270
|
-
}
|
|
271
|
-
|
|
222
|
+
}, {
|
|
223
|
+
"user-wallet": this.userWallet,
|
|
224
|
+
});
|
|
272
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Load Loopscale loan positions.
|
|
228
|
+
*/
|
|
273
229
|
async getLoanInfo(params) {
|
|
274
|
-
return this.post("/markets/loans/info", params
|
|
230
|
+
return this.post("/markets/loans/info", params);
|
|
275
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Load Loopscale lending strategies.
|
|
234
|
+
*/
|
|
276
235
|
async getStrategies(params) {
|
|
277
|
-
return this.post("/markets/strategy/infos", params
|
|
278
|
-
}
|
|
279
|
-
async getCollateralHolders(params) {
|
|
280
|
-
return this.post("/markets/collateral/holders", params);
|
|
236
|
+
return this.post("/markets/strategy/infos", params);
|
|
281
237
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
async getVaultDepositors(params) {
|
|
286
|
-
return this.post("/markets/lending_vaults/deposits", params);
|
|
287
|
-
}
|
|
288
|
-
async getVaultInfo(params) {
|
|
289
|
-
return this.post("/markets/lending_vaults/info", params);
|
|
290
|
-
}
|
|
291
|
-
async getLoopInfo(params) {
|
|
292
|
-
return this.post("/markets/loop/info", params);
|
|
293
|
-
}
|
|
294
|
-
// ── Strategy (LENDER side) ──
|
|
238
|
+
/**
|
|
239
|
+
* Build a raw Loopscale `createStrategy` transaction batch.
|
|
240
|
+
*/
|
|
295
241
|
async createStrategy(params) {
|
|
296
|
-
const lender = toBase58(params.lender);
|
|
297
242
|
const body = {
|
|
298
243
|
principalMint: toBase58(params.principalMint),
|
|
299
|
-
lender,
|
|
244
|
+
lender: toBase58(params.lender),
|
|
300
245
|
amount: toSafeNumber(params.amount, "amount"),
|
|
301
246
|
};
|
|
302
247
|
if (params.originationsEnabled !== undefined)
|
|
@@ -315,180 +260,69 @@ class LoopscaleClient {
|
|
|
315
260
|
body.marketInformation = toBase58(params.marketInformation);
|
|
316
261
|
if (params.externalYieldSourceArgs)
|
|
317
262
|
body.externalYieldSourceArgs = params.externalYieldSourceArgs;
|
|
318
|
-
|
|
319
|
-
if (!Array.isArray(entries) || entries.length === 0) {
|
|
320
|
-
throw new Error("Loopscale strategy/create returned no transactions");
|
|
321
|
-
}
|
|
322
|
-
const batches = await this.parseStrategyBatches(entries);
|
|
323
|
-
const createBatch = batches.find((batch) => batch.raw.loopscaleInstructions.some((g) => g.name === "createStrategy"));
|
|
324
|
-
const createIx = createBatch?.raw.loopscaleInstructions.find((g) => g.name === "createStrategy");
|
|
325
|
-
if (!createIx)
|
|
326
|
-
throw new Error("Loopscale strategy/create did not return a createStrategy instruction");
|
|
327
|
-
const strategyAddress = createIx.instruction.keys[3]?.pubkey;
|
|
328
|
-
const noncePublicKey = createIx.instruction.keys[2]?.pubkey;
|
|
329
|
-
if (!strategyAddress || !noncePublicKey) {
|
|
330
|
-
throw new Error("Invalid createStrategy account layout from API");
|
|
331
|
-
}
|
|
332
|
-
if (this.debug) {
|
|
333
|
-
console.log(`[LoopscaleClient] createStrategy: strategy=${strategyAddress.toBase58()}, batches=${batches.length}`);
|
|
334
|
-
}
|
|
335
|
-
return { batches, strategyAddress, noncePublicKey };
|
|
263
|
+
return this.post("/markets/strategy/create", body, { payer: this.payer });
|
|
336
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Build a raw Loopscale `depositStrategy` transaction.
|
|
267
|
+
*/
|
|
337
268
|
async depositStrategy(params) {
|
|
338
|
-
|
|
269
|
+
return this.post("/markets/strategy/deposit", {
|
|
339
270
|
strategy: toBase58(params.strategy),
|
|
340
271
|
amount: toSafeNumber(params.amount, "amount"),
|
|
341
|
-
}
|
|
342
|
-
return this.fetchAndClassifySingleTx("/markets/strategy/deposit", body, {
|
|
272
|
+
}, withPayerHeader({
|
|
343
273
|
"User-Wallet": this.userWallet,
|
|
344
|
-
|
|
345
|
-
});
|
|
274
|
+
}, this.payer));
|
|
346
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* Build a raw Loopscale `withdrawStrategy` transaction.
|
|
278
|
+
*/
|
|
347
279
|
async withdrawStrategy(params) {
|
|
348
|
-
|
|
280
|
+
return this.post("/markets/strategy/withdraw", {
|
|
349
281
|
strategy: toBase58(params.strategy),
|
|
350
282
|
amount: toSafeNumber(params.amount, "amount"),
|
|
351
283
|
withdrawAll: params.withdrawAll,
|
|
352
|
-
}
|
|
353
|
-
return this.fetchAndClassifySingleTx("/markets/strategy/withdraw", body, {
|
|
284
|
+
}, withPayerHeader({
|
|
354
285
|
"User-Wallet": this.userWallet,
|
|
355
|
-
|
|
356
|
-
});
|
|
286
|
+
}, this.payer));
|
|
357
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Build a raw Loopscale `closeStrategy` transaction.
|
|
290
|
+
*/
|
|
358
291
|
async closeStrategy(params) {
|
|
359
|
-
|
|
360
|
-
return this.fetchAndClassifySingleTx(`/markets/strategy/close/${strategy}`, undefined, {
|
|
361
|
-
payer: this.userWallet,
|
|
362
|
-
});
|
|
292
|
+
return this.post(`/markets/strategy/close/${toBase58(params.strategy)}`, undefined, { payer: this.payer });
|
|
363
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Build raw Loopscale `updateStrategy` transactions.
|
|
296
|
+
*/
|
|
364
297
|
async updateStrategy(params) {
|
|
365
|
-
const body = {
|
|
298
|
+
const body = {
|
|
299
|
+
strategy: toBase58(params.strategy),
|
|
300
|
+
};
|
|
366
301
|
if (params.collateralTerms)
|
|
367
302
|
body.collateralTerms = params.collateralTerms;
|
|
368
303
|
if (params.updateParams)
|
|
369
304
|
body.updateParams = params.updateParams;
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
const batches = await this.parseStrategyBatches(entries);
|
|
375
|
-
if (this.debug) {
|
|
376
|
-
console.log(`[LoopscaleClient] updateStrategy: batches=${batches.length}`);
|
|
377
|
-
}
|
|
378
|
-
return { batches };
|
|
379
|
-
}
|
|
380
|
-
// ── Loan (BORROWER side) ──
|
|
381
|
-
/**
|
|
382
|
-
* Build a standalone borrow_principal instruction (no lock/unlock needed).
|
|
383
|
-
* Requires collateral to already be deposited in the loan.
|
|
384
|
-
*/
|
|
385
|
-
buildBorrowPrincipalInstruction(params) {
|
|
386
|
-
const borrower = new web3_js_1.PublicKey(toBase58(params.borrower));
|
|
387
|
-
const loan = new web3_js_1.PublicKey(toBase58(params.loan));
|
|
388
|
-
const strategy = new web3_js_1.PublicKey(toBase58(params.strategy));
|
|
389
|
-
const marketInformation = new web3_js_1.PublicKey(toBase58(params.marketInformation));
|
|
390
|
-
const principalMint = new web3_js_1.PublicKey(toBase58(params.principalMint));
|
|
391
|
-
const amount = typeof params.amount === "bigint" ? params.amount : BigInt(params.amount);
|
|
392
|
-
const guidance = params.assetIndexGuidance;
|
|
393
|
-
const duration = params.durationIndex;
|
|
394
|
-
const expectedApy = params.expectedApy !== undefined
|
|
395
|
-
? (typeof params.expectedApy === "bigint" ? params.expectedApy : BigInt(params.expectedApy))
|
|
396
|
-
: 0n;
|
|
397
|
-
const expectedLqt = params.expectedLqt ?? [0, 0, 0, 0, 0];
|
|
398
|
-
const skipSolUnwrap = params.skipSolUnwrap ?? false;
|
|
399
|
-
// Serialize instruction data: discriminator + amount + guidance_vec + duration + expected_loan_values + skip_sol_unwrap
|
|
400
|
-
const dataSize = 8 + 8 + 4 + guidance.length + 1 + 8 + 20 + 1;
|
|
401
|
-
const data = Buffer.alloc(dataSize);
|
|
402
|
-
let offset = 0;
|
|
403
|
-
Buffer.from(policyBuilders_1.LOOPSCALE_DISCRIMINATORS.borrowPrincipal).copy(data, 0);
|
|
404
|
-
offset += 8;
|
|
405
|
-
data.writeBigUInt64LE(amount, offset);
|
|
406
|
-
offset += 8;
|
|
407
|
-
data.writeUInt32LE(guidance.length, offset);
|
|
408
|
-
offset += 4;
|
|
409
|
-
for (const b of guidance) {
|
|
410
|
-
data.writeUInt8(b, offset);
|
|
411
|
-
offset += 1;
|
|
412
|
-
}
|
|
413
|
-
data.writeUInt8(duration, offset);
|
|
414
|
-
offset += 1;
|
|
415
|
-
data.writeBigUInt64LE(expectedApy, offset);
|
|
416
|
-
offset += 8;
|
|
417
|
-
for (let i = 0; i < 5; i++) {
|
|
418
|
-
data.writeUInt32LE(expectedLqt[i] ?? 0, offset);
|
|
419
|
-
offset += 4;
|
|
420
|
-
}
|
|
421
|
-
data.writeUInt8(skipSolUnwrap ? 1 : 0, offset);
|
|
422
|
-
offset += 1;
|
|
423
|
-
const TOKEN_PROGRAM_ID = new web3_js_1.PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
|
|
424
|
-
const [borrowerTa] = web3_js_1.PublicKey.findProgramAddressSync([borrower.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), principalMint.toBuffer()], ASSOCIATED_TOKEN_PROGRAM_ID);
|
|
425
|
-
const [strategyTa] = web3_js_1.PublicKey.findProgramAddressSync([strategy.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), principalMint.toBuffer()], ASSOCIATED_TOKEN_PROGRAM_ID);
|
|
426
|
-
const eventAuthority = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("__event_authority")], policyBuilders_1.LOOPSCALE_PROGRAM_ID)[0];
|
|
427
|
-
const ix = new web3_js_1.TransactionInstruction({
|
|
428
|
-
programId: policyBuilders_1.LOOPSCALE_PROGRAM_ID,
|
|
429
|
-
keys: [
|
|
430
|
-
{ pubkey: this.bsAuth, isSigner: true, isWritable: false },
|
|
431
|
-
{ pubkey: borrower, isSigner: true, isWritable: true },
|
|
432
|
-
{ pubkey: borrower, isSigner: true, isWritable: false },
|
|
433
|
-
{ pubkey: loan, isSigner: false, isWritable: true },
|
|
434
|
-
{ pubkey: strategy, isSigner: false, isWritable: true },
|
|
435
|
-
{ pubkey: marketInformation, isSigner: false, isWritable: true },
|
|
436
|
-
{ pubkey: principalMint, isSigner: false, isWritable: false },
|
|
437
|
-
{ pubkey: borrowerTa, isSigner: false, isWritable: true },
|
|
438
|
-
{ pubkey: strategyTa, isSigner: false, isWritable: true },
|
|
439
|
-
{ pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
|
440
|
-
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
|
441
|
-
{ pubkey: web3_js_1.SystemProgram.programId, isSigner: false, isWritable: false },
|
|
442
|
-
{ pubkey: eventAuthority, isSigner: false, isWritable: false },
|
|
443
|
-
{ pubkey: policyBuilders_1.LOOPSCALE_PROGRAM_ID, isSigner: false, isWritable: false },
|
|
444
|
-
...params.healthCheckAccounts,
|
|
445
|
-
],
|
|
446
|
-
data,
|
|
447
|
-
});
|
|
448
|
-
return ix;
|
|
305
|
+
return this.post("/markets/strategy/update", body, withPayerHeader({
|
|
306
|
+
"User-Wallet": this.userWallet,
|
|
307
|
+
}, this.payer));
|
|
449
308
|
}
|
|
450
309
|
/**
|
|
451
|
-
* Build a
|
|
452
|
-
* Requires collateral to already be deposited in the loan.
|
|
310
|
+
* Build a raw Loopscale `createLoan` transaction.
|
|
453
311
|
*/
|
|
454
|
-
buildBorrowPrincipalAction(params) {
|
|
455
|
-
return vault_interaction_1.loopscaleAction.borrowPrincipal({ instruction: this.buildBorrowPrincipalInstruction(params) });
|
|
456
|
-
}
|
|
457
|
-
async repayLoan(params) {
|
|
458
|
-
const body = {
|
|
459
|
-
loan: toBase58(params.loan),
|
|
460
|
-
repayParams: params.repayParams.map((p) => ({
|
|
461
|
-
amount: toSafeNumber(p.amount, "repayAmount"),
|
|
462
|
-
ledgerIndex: p.ledgerIndex,
|
|
463
|
-
repayAll: p.repayAll,
|
|
464
|
-
})),
|
|
465
|
-
collateralWithdrawalParams: (params.collateralWithdrawalParams ?? []).map((p) => ({
|
|
466
|
-
amount: toSafeNumber(p.amount, "withdrawAmount"),
|
|
467
|
-
collateralMint: toBase58(p.collateralMint),
|
|
468
|
-
})),
|
|
469
|
-
};
|
|
470
|
-
if (params.closeIfPossible !== undefined)
|
|
471
|
-
body.closeIfPossible = params.closeIfPossible;
|
|
472
|
-
return this.fetchAndClassifyWrappedTx("/markets/creditbook/repay", body, { "user-wallet": this.userWallet });
|
|
473
|
-
}
|
|
474
|
-
async closeLoan(params) {
|
|
475
|
-
const body = { loan: toBase58(params.loan) };
|
|
476
|
-
return this.fetchAndClassifyWrappedTx("/markets/creditbook/close_loan", body, { "user-wallet": this.userWallet });
|
|
477
|
-
}
|
|
478
312
|
async createLoan(params) {
|
|
479
313
|
const body = {
|
|
480
314
|
borrower: toBase58(params.borrower),
|
|
481
|
-
depositCollateral: params.depositCollateral.map((
|
|
482
|
-
collateralAmount: toSafeNumber(
|
|
483
|
-
collateralAssetData:
|
|
315
|
+
depositCollateral: params.depositCollateral.map((item) => ({
|
|
316
|
+
collateralAmount: toSafeNumber(item.collateralAmount, "collateralAmount"),
|
|
317
|
+
collateralAssetData: item.collateralAssetData,
|
|
484
318
|
})),
|
|
485
|
-
principalRequested: params.principalRequested.map((
|
|
486
|
-
ledgerIndex:
|
|
487
|
-
principalAmount: toSafeNumber(
|
|
488
|
-
principalMint: toBase58(
|
|
489
|
-
strategy: toBase58(
|
|
490
|
-
durationIndex:
|
|
491
|
-
expectedLoanValues:
|
|
319
|
+
principalRequested: params.principalRequested.map((item) => ({
|
|
320
|
+
ledgerIndex: item.ledgerIndex,
|
|
321
|
+
principalAmount: toSafeNumber(item.principalAmount, "principalAmount"),
|
|
322
|
+
principalMint: toBase58(item.principalMint),
|
|
323
|
+
strategy: toBase58(item.strategy),
|
|
324
|
+
durationIndex: item.durationIndex,
|
|
325
|
+
expectedLoanValues: item.expectedLoanValues ?? { expectedApy: 0, expectedLqt: [0, 0, 0, 0, 0] },
|
|
492
326
|
})),
|
|
493
327
|
};
|
|
494
328
|
if (params.assetIndexGuidance)
|
|
@@ -497,38 +331,40 @@ class LoopscaleClient {
|
|
|
497
331
|
body.loanNonce = params.loanNonce;
|
|
498
332
|
if (params.isLoop !== undefined)
|
|
499
333
|
body.isLoop = params.isLoop;
|
|
500
|
-
|
|
501
|
-
const data = await this.post("/markets/creditbook/create", body, headers);
|
|
502
|
-
const tx = buildTxFromLegacyEntry(data.transaction);
|
|
503
|
-
const instructions = await decompileInstructions(this.connection, tx);
|
|
504
|
-
const classified = this.classifyInstructions(instructions);
|
|
505
|
-
const syncActions = this.buildSyncActions(classified.loopscale);
|
|
506
|
-
const raw = this.buildRawBundle(tx, instructions, classified.loopscale);
|
|
507
|
-
const loanAddress = new web3_js_1.PublicKey(data.loanAddress);
|
|
508
|
-
if (this.debug) {
|
|
509
|
-
console.log(`[LoopscaleClient] createLoan: loan=${loanAddress.toBase58()}, syncActions=${syncActions.length}`);
|
|
510
|
-
}
|
|
511
|
-
return { syncActions, topLevelInstructions: classified.topLevel, raw, loanAddress };
|
|
334
|
+
return this.post("/markets/creditbook/create", body, { payer: this.payer });
|
|
512
335
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
336
|
+
/**
|
|
337
|
+
* Build a raw Loopscale `borrowPrincipal` transaction.
|
|
338
|
+
*
|
|
339
|
+
* The published OpenAPI schema currently documents a single serialized
|
|
340
|
+
* transaction response, but live responses can also return a wrapped
|
|
341
|
+
* `transactions` array with `expectedLoanInfo` when Loopscale inserts
|
|
342
|
+
* additional internal maintenance steps.
|
|
343
|
+
*/
|
|
344
|
+
async borrowPrincipal(params) {
|
|
521
345
|
const body = {
|
|
522
346
|
loan: toBase58(params.loan),
|
|
523
|
-
borrowParams
|
|
347
|
+
borrowParams: {
|
|
348
|
+
amount: toSafeNumber(params.borrowParams.amount, "amount"),
|
|
349
|
+
duration: params.borrowParams.durationIndex,
|
|
350
|
+
},
|
|
524
351
|
strategy: toBase58(params.strategy),
|
|
525
352
|
};
|
|
353
|
+
if (params.borrowParams.expectedLoanValues) {
|
|
354
|
+
body.borrowParams = {
|
|
355
|
+
...body.borrowParams,
|
|
356
|
+
expectedLoanValues: params.borrowParams.expectedLoanValues,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
526
359
|
if (params.refinanceParams)
|
|
527
360
|
body.refinanceParams = params.refinanceParams;
|
|
528
361
|
if (params.isLoop !== undefined)
|
|
529
362
|
body.isLoop = params.isLoop;
|
|
530
|
-
return this.
|
|
363
|
+
return this.post("/markets/creditbook/borrow", body, { payer: this.payer });
|
|
531
364
|
}
|
|
365
|
+
/**
|
|
366
|
+
* Build a raw Loopscale `depositCollateral` transaction.
|
|
367
|
+
*/
|
|
532
368
|
async depositCollateral(params) {
|
|
533
369
|
const body = {
|
|
534
370
|
loan: toBase58(params.loan),
|
|
@@ -541,151 +377,69 @@ class LoopscaleClient {
|
|
|
541
377
|
body.assetIndexGuidance = params.assetIndexGuidance;
|
|
542
378
|
if (params.expectedLoanValues)
|
|
543
379
|
body.expectedLoanValues = params.expectedLoanValues;
|
|
544
|
-
return this.
|
|
380
|
+
return this.post("/markets/creditbook/collateral/deposit", body, withPayerHeader({
|
|
545
381
|
"user-wallet": this.userWallet,
|
|
546
|
-
|
|
547
|
-
});
|
|
382
|
+
}, this.payer));
|
|
548
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* Build a raw Loopscale `withdrawCollateral` transaction.
|
|
386
|
+
*
|
|
387
|
+
* Full collateral removal can trigger internal loan-maintenance instructions,
|
|
388
|
+
* so callers should accept either a single serialized transaction or a wrapped
|
|
389
|
+
* `transactions` array.
|
|
390
|
+
*/
|
|
549
391
|
async withdrawCollateral(params) {
|
|
550
392
|
const body = {
|
|
551
393
|
loan: toBase58(params.loan),
|
|
552
394
|
collateralMint: toBase58(params.collateralMint),
|
|
553
395
|
amount: toSafeNumber(params.amount, "amount"),
|
|
554
396
|
collateralIndex: params.collateralIndex,
|
|
397
|
+
expectedLoanValues: params.expectedLoanValues,
|
|
555
398
|
};
|
|
556
|
-
if (params.expectedLoanValues)
|
|
557
|
-
body.expectedLoanValues = params.expectedLoanValues;
|
|
558
399
|
if (params.assetIndexGuidance)
|
|
559
400
|
body.assetIndexGuidance = params.assetIndexGuidance;
|
|
560
|
-
return this.
|
|
561
|
-
"user-wallet": this.userWallet,
|
|
562
|
-
payer: this.userWallet,
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
async refinanceLoan(params) {
|
|
566
|
-
const body = {
|
|
567
|
-
loan: toBase58(params.loan),
|
|
568
|
-
oldStrategy: toBase58(params.oldStrategy),
|
|
569
|
-
newStrategy: toBase58(params.newStrategy),
|
|
570
|
-
refinanceParams: params.refinanceParams,
|
|
571
|
-
};
|
|
572
|
-
return this.fetchAndClassifyWrappedTx("/markets/creditbook/refinance", body, {
|
|
401
|
+
return this.post("/markets/creditbook/collateral/withdraw", body, withPayerHeader({
|
|
573
402
|
"user-wallet": this.userWallet,
|
|
574
|
-
});
|
|
403
|
+
}, this.payer));
|
|
575
404
|
}
|
|
576
|
-
|
|
405
|
+
/**
|
|
406
|
+
* Co-sign a prepared transaction with Loopscale's back-end signer.
|
|
407
|
+
*
|
|
408
|
+
* This uses Loopscale's public MPC co-signing endpoint. The returned
|
|
409
|
+
* transaction is required to keep the exact same message as the locally
|
|
410
|
+
* signed input, and any local signatures that are missing from the response
|
|
411
|
+
* are copied back onto the returned transaction before it is handed to the
|
|
412
|
+
* caller.
|
|
413
|
+
*/
|
|
577
414
|
async coSign(transaction) {
|
|
578
|
-
const
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
}],
|
|
588
|
-
}],
|
|
589
|
-
};
|
|
590
|
-
const data = await this.post("/mpc/txns/gen", body);
|
|
591
|
-
const txEntry = data.batches?.[0]?.transactions?.[0];
|
|
592
|
-
if (!txEntry)
|
|
593
|
-
throw new Error("Loopscale MPC co-signing returned no transaction");
|
|
594
|
-
const resultTx = web3_js_1.VersionedTransaction.deserialize(Buffer.from(txEntry.transaction, "base64"));
|
|
595
|
-
ensureMessageMatches(resultTx, transaction);
|
|
596
|
-
return mergeInputSignatures(resultTx, transaction);
|
|
597
|
-
}
|
|
598
|
-
// Legacy endpoint
|
|
599
|
-
const body = [{ transaction: serializedTx, message: serializedMsg }];
|
|
600
|
-
const entries = await this.post("/markets/txn/gen", body, { "user-wallet": this.userWallet });
|
|
601
|
-
if (!entries?.[0])
|
|
602
|
-
throw new Error("Loopscale co-signing returned no entries");
|
|
603
|
-
const resultTx = buildTxFromLegacyEntry(entries[0]);
|
|
604
|
-
ensureMessageMatches(resultTx, transaction);
|
|
605
|
-
return mergeInputSignatures(resultTx, transaction);
|
|
606
|
-
}
|
|
607
|
-
// ── Instruction helpers ──
|
|
608
|
-
classifyInstructions(instructions) {
|
|
609
|
-
const loopscale = [];
|
|
610
|
-
const topLevel = [];
|
|
611
|
-
const setup = [];
|
|
612
|
-
for (const ix of instructions) {
|
|
613
|
-
if (ix.programId.equals(policyBuilders_1.LOOPSCALE_PROGRAM_ID)) {
|
|
614
|
-
loopscale.push({ name: identifyInstruction(ix), instruction: ix });
|
|
615
|
-
}
|
|
616
|
-
else if (ix.programId.equals(web3_js_1.ComputeBudgetProgram.programId)) {
|
|
617
|
-
topLevel.push(ix);
|
|
618
|
-
}
|
|
619
|
-
else if (ix.programId.equals(ASSOCIATED_TOKEN_PROGRAM_ID)) {
|
|
620
|
-
setup.push(ix);
|
|
621
|
-
}
|
|
622
|
-
else {
|
|
623
|
-
// Oracle feeds, system program, etc. — top level
|
|
624
|
-
topLevel.push(ix);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
return { loopscale, topLevel, setup };
|
|
628
|
-
}
|
|
629
|
-
static identifyInstruction(ix) {
|
|
630
|
-
return identifyInstruction(ix);
|
|
631
|
-
}
|
|
632
|
-
// ── Private helpers ──
|
|
633
|
-
buildSyncActions(loopscaleIxs) {
|
|
634
|
-
return loopscaleIxs
|
|
635
|
-
.filter((g) => g.name !== null)
|
|
636
|
-
.map((g) => {
|
|
637
|
-
const wrapper = NAME_TO_ACTION_WRAPPER[g.name];
|
|
638
|
-
return wrapper(g.instruction);
|
|
415
|
+
const serializedTransaction = Buffer.from(transaction.serialize()).toString("base64");
|
|
416
|
+
const data = await this.post("/mpc/txns/gen", {
|
|
417
|
+
batches: [{
|
|
418
|
+
transactions: [{
|
|
419
|
+
identifier: "loopscale-cosign",
|
|
420
|
+
transaction: serializedTransaction,
|
|
421
|
+
transactionType: 1,
|
|
422
|
+
}],
|
|
423
|
+
}],
|
|
639
424
|
});
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
}
|
|
644
|
-
toSingleTxResult(batch) {
|
|
645
|
-
return {
|
|
646
|
-
syncActions: batch.syncActions,
|
|
647
|
-
topLevelInstructions: batch.topLevelInstructions,
|
|
648
|
-
raw: batch.raw,
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
|
-
async parseTransactionEntry(entry) {
|
|
652
|
-
const tx = buildTxFromLegacyEntry(entry);
|
|
653
|
-
const instructions = await decompileInstructions(this.connection, tx);
|
|
654
|
-
const classified = this.classifyInstructions(instructions);
|
|
655
|
-
const syncActions = this.buildSyncActions(classified.loopscale);
|
|
656
|
-
const raw = this.buildRawBundle(tx, instructions, classified.loopscale);
|
|
657
|
-
return {
|
|
658
|
-
syncActions,
|
|
659
|
-
topLevelInstructions: classified.topLevel,
|
|
660
|
-
setupInstructions: classified.setup,
|
|
661
|
-
raw,
|
|
662
|
-
};
|
|
663
|
-
}
|
|
664
|
-
async parseStrategyBatches(entries) {
|
|
665
|
-
return Promise.all(entries.map((entry) => this.parseTransactionEntry(entry)));
|
|
666
|
-
}
|
|
667
|
-
async fetchAndClassifyWrappedTx(path, body, headers) {
|
|
668
|
-
const data = await this.post(path, body, headers);
|
|
669
|
-
if (!data.transactions?.length) {
|
|
670
|
-
throw new Error(`Loopscale ${path} returned no transactions`);
|
|
671
|
-
}
|
|
672
|
-
if (data.transactions.length > 1) {
|
|
673
|
-
throw new Error(`Loopscale ${path} returned ${data.transactions.length} transactions — expected 1. ` +
|
|
674
|
-
`Multi-transaction responses (e.g. from setupIxs) are not supported.`);
|
|
675
|
-
}
|
|
676
|
-
const batch = await this.parseTransactionEntry(data.transactions[0]);
|
|
677
|
-
if (this.debug) {
|
|
678
|
-
console.log(`[LoopscaleClient] ${path}: syncActions=${batch.syncActions.length}, topLevel=${batch.topLevelInstructions.length}`);
|
|
425
|
+
const transactionEntry = data.batches?.[0]?.transactions?.[0];
|
|
426
|
+
if (!transactionEntry) {
|
|
427
|
+
throw new Error("Loopscale MPC co-signing returned no transaction");
|
|
679
428
|
}
|
|
680
|
-
|
|
429
|
+
const coSignedTransaction = web3_js_1.VersionedTransaction.deserialize(Buffer.from(transactionEntry.transaction, "base64"));
|
|
430
|
+
return mergeCoSignedTransaction(transaction, coSignedTransaction);
|
|
681
431
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
432
|
+
/**
|
|
433
|
+
* Extract the strategy PDA from a `createStrategy` response using the client's connection.
|
|
434
|
+
*/
|
|
435
|
+
async extractCreatedStrategyAddress(response) {
|
|
436
|
+
return extractCreatedStrategyAddress(this.connection, response);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Extract the strategy nonce from a `createStrategy` response using the client's connection.
|
|
440
|
+
*/
|
|
441
|
+
async extractCreatedStrategyNonce(response) {
|
|
442
|
+
return extractCreatedStrategyNonce(this.connection, response);
|
|
689
443
|
}
|
|
690
444
|
async post(path, body, extraHeaders) {
|
|
691
445
|
const url = `${this.baseUrl}${path}`;
|
|
@@ -695,31 +449,34 @@ class LoopscaleClient {
|
|
|
695
449
|
};
|
|
696
450
|
if (this.debug) {
|
|
697
451
|
console.log(`\n[LoopscaleClient] POST ${url}`);
|
|
698
|
-
if (body)
|
|
452
|
+
if (body !== undefined) {
|
|
699
453
|
console.log(` body: ${JSON.stringify(body, null, 2).split("\n").slice(0, 10).join("\n")}...`);
|
|
454
|
+
}
|
|
700
455
|
}
|
|
701
456
|
let response;
|
|
702
457
|
const maxRetries = 3;
|
|
703
|
-
for (let attempt = 0; attempt <= maxRetries; attempt
|
|
458
|
+
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
704
459
|
response = await fetch(url, {
|
|
705
460
|
method: "POST",
|
|
706
461
|
headers,
|
|
707
462
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
708
463
|
});
|
|
709
464
|
if (response.status === 429 && attempt < maxRetries) {
|
|
710
|
-
const
|
|
711
|
-
if (this.debug)
|
|
712
|
-
console.log(` RATE LIMITED (429), retrying in ${
|
|
713
|
-
|
|
465
|
+
const delayMs = 2_000 * (attempt + 1);
|
|
466
|
+
if (this.debug) {
|
|
467
|
+
console.log(` RATE LIMITED (429), retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries})...`);
|
|
468
|
+
}
|
|
469
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
714
470
|
continue;
|
|
715
471
|
}
|
|
716
472
|
break;
|
|
717
473
|
}
|
|
718
|
-
if (!response
|
|
719
|
-
const text = await response
|
|
720
|
-
if (this.debug)
|
|
721
|
-
console.log(` ERROR ${response
|
|
722
|
-
|
|
474
|
+
if (!response?.ok) {
|
|
475
|
+
const text = await response?.text();
|
|
476
|
+
if (this.debug) {
|
|
477
|
+
console.log(` ERROR ${response?.status}: ${text}`);
|
|
478
|
+
}
|
|
479
|
+
throw new Error(`Loopscale API ${path} failed (${response?.status}): ${text}`);
|
|
723
480
|
}
|
|
724
481
|
const data = await response.json();
|
|
725
482
|
if (this.debug) {
|