@exponent-labs/exponent-sdk 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/build/client/vaults/index.d.ts +2 -0
  2. package/build/client/vaults/index.js +2 -0
  3. package/build/client/vaults/index.js.map +1 -1
  4. package/build/client/vaults/types/index.d.ts +2 -0
  5. package/build/client/vaults/types/index.js +2 -0
  6. package/build/client/vaults/types/index.js.map +1 -1
  7. package/build/client/vaults/types/kaminoFarmEntry.d.ts +15 -0
  8. package/build/client/vaults/types/kaminoFarmEntry.js +17 -0
  9. package/build/client/vaults/types/kaminoFarmEntry.js.map +1 -0
  10. package/build/client/vaults/types/kaminoObligationEntry.d.ts +21 -4
  11. package/build/client/vaults/types/kaminoObligationEntry.js +2 -1
  12. package/build/client/vaults/types/kaminoObligationEntry.js.map +1 -1
  13. package/build/client/vaults/types/positionUpdate.d.ts +9 -0
  14. package/build/client/vaults/types/positionUpdate.js +23 -0
  15. package/build/client/vaults/types/positionUpdate.js.map +1 -1
  16. package/build/client/vaults/types/proposalAction.js +0 -3
  17. package/build/client/vaults/types/proposalAction.js.map +1 -1
  18. package/build/client/vaults/types/reserveFarmMapping.d.ts +19 -0
  19. package/build/client/vaults/types/reserveFarmMapping.js +18 -0
  20. package/build/client/vaults/types/reserveFarmMapping.js.map +1 -0
  21. package/build/client/vaults/types/strategyPosition.d.ts +5 -0
  22. package/build/client/vaults/types/strategyPosition.js +5 -0
  23. package/build/client/vaults/types/strategyPosition.js.map +1 -1
  24. package/build/exponentVaults/aumCalculator.d.ts +25 -4
  25. package/build/exponentVaults/aumCalculator.js +236 -15
  26. package/build/exponentVaults/aumCalculator.js.map +1 -1
  27. package/build/exponentVaults/fetcher.d.ts +52 -0
  28. package/build/exponentVaults/fetcher.js +199 -0
  29. package/build/exponentVaults/fetcher.js.map +1 -0
  30. package/build/exponentVaults/index.d.ts +10 -9
  31. package/build/exponentVaults/index.js +26 -8
  32. package/build/exponentVaults/index.js.map +1 -1
  33. package/build/exponentVaults/kamino-farms.d.ts +144 -0
  34. package/build/exponentVaults/kamino-farms.js +396 -0
  35. package/build/exponentVaults/kamino-farms.js.map +1 -0
  36. package/build/exponentVaults/loopscale/client.d.ts +240 -0
  37. package/build/exponentVaults/loopscale/client.js +590 -0
  38. package/build/exponentVaults/loopscale/client.js.map +1 -0
  39. package/build/exponentVaults/loopscale/client.test.d.ts +1 -0
  40. package/build/exponentVaults/loopscale/client.test.js +183 -0
  41. package/build/exponentVaults/loopscale/client.test.js.map +1 -0
  42. package/build/exponentVaults/loopscale/helpers.d.ts +29 -0
  43. package/build/exponentVaults/loopscale/helpers.js +119 -0
  44. package/build/exponentVaults/loopscale/helpers.js.map +1 -0
  45. package/build/exponentVaults/loopscale/index.d.ts +3 -0
  46. package/build/exponentVaults/loopscale/index.js +12 -0
  47. package/build/exponentVaults/loopscale/index.js.map +1 -0
  48. package/build/exponentVaults/loopscale/prepared-transactions.d.ts +13 -0
  49. package/build/exponentVaults/loopscale/prepared-transactions.js +271 -0
  50. package/build/exponentVaults/loopscale/prepared-transactions.js.map +1 -0
  51. package/build/exponentVaults/loopscale/prepared-transactions.test.d.ts +1 -0
  52. package/build/exponentVaults/loopscale/prepared-transactions.test.js +400 -0
  53. package/build/exponentVaults/loopscale/prepared-transactions.test.js.map +1 -0
  54. package/build/exponentVaults/loopscale/prepared-types.d.ts +62 -0
  55. package/build/exponentVaults/loopscale/prepared-types.js +3 -0
  56. package/build/exponentVaults/loopscale/prepared-types.js.map +1 -0
  57. package/build/exponentVaults/loopscale/response-plan.d.ts +69 -0
  58. package/build/exponentVaults/loopscale/response-plan.js +141 -0
  59. package/build/exponentVaults/loopscale/response-plan.js.map +1 -0
  60. package/build/exponentVaults/loopscale/response-plan.test.d.ts +1 -0
  61. package/build/exponentVaults/loopscale/response-plan.test.js +139 -0
  62. package/build/exponentVaults/loopscale/response-plan.test.js.map +1 -0
  63. package/build/exponentVaults/loopscale/send-plan.d.ts +75 -0
  64. package/build/exponentVaults/loopscale/send-plan.js +235 -0
  65. package/build/exponentVaults/loopscale/send-plan.js.map +1 -0
  66. package/build/exponentVaults/loopscale/types.d.ts +443 -0
  67. package/build/exponentVaults/loopscale/types.js +3 -0
  68. package/build/exponentVaults/loopscale/types.js.map +1 -0
  69. package/build/exponentVaults/loopscale-client.d.ts +113 -524
  70. package/build/exponentVaults/loopscale-client.js +296 -539
  71. package/build/exponentVaults/loopscale-client.js.map +1 -1
  72. package/build/exponentVaults/loopscale-client.test.d.ts +1 -0
  73. package/build/exponentVaults/loopscale-client.test.js +162 -0
  74. package/build/exponentVaults/loopscale-client.test.js.map +1 -0
  75. package/build/exponentVaults/loopscale-client.types.d.ts +425 -0
  76. package/build/exponentVaults/loopscale-client.types.js +3 -0
  77. package/build/exponentVaults/loopscale-client.types.js.map +1 -0
  78. package/build/exponentVaults/loopscale-execution.d.ts +125 -0
  79. package/build/exponentVaults/loopscale-execution.js +341 -0
  80. package/build/exponentVaults/loopscale-execution.js.map +1 -0
  81. package/build/exponentVaults/loopscale-execution.test.d.ts +1 -0
  82. package/build/exponentVaults/loopscale-execution.test.js +139 -0
  83. package/build/exponentVaults/loopscale-execution.test.js.map +1 -0
  84. package/build/exponentVaults/loopscale-vault.d.ts +115 -0
  85. package/build/exponentVaults/loopscale-vault.js +275 -0
  86. package/build/exponentVaults/loopscale-vault.js.map +1 -0
  87. package/build/exponentVaults/loopscale-vault.test.d.ts +1 -0
  88. package/build/exponentVaults/loopscale-vault.test.js +102 -0
  89. package/build/exponentVaults/loopscale-vault.test.js.map +1 -0
  90. package/build/exponentVaults/policyBuilders.d.ts +62 -0
  91. package/build/exponentVaults/policyBuilders.js +119 -2
  92. package/build/exponentVaults/policyBuilders.js.map +1 -1
  93. package/build/exponentVaults/pricePathResolver.d.ts +45 -0
  94. package/build/exponentVaults/pricePathResolver.js +198 -0
  95. package/build/exponentVaults/pricePathResolver.js.map +1 -0
  96. package/build/exponentVaults/pricePathResolver.test.d.ts +1 -0
  97. package/build/exponentVaults/pricePathResolver.test.js +369 -0
  98. package/build/exponentVaults/pricePathResolver.test.js.map +1 -0
  99. package/build/exponentVaults/syncTransaction.js +4 -1
  100. package/build/exponentVaults/syncTransaction.js.map +1 -1
  101. package/build/exponentVaults/titan-quote.js +170 -36
  102. package/build/exponentVaults/titan-quote.js.map +1 -1
  103. package/build/exponentVaults/vault-instruction-types.d.ts +363 -0
  104. package/build/exponentVaults/vault-instruction-types.js +128 -0
  105. package/build/exponentVaults/vault-instruction-types.js.map +1 -0
  106. package/build/exponentVaults/vault-interaction.d.ts +203 -343
  107. package/build/exponentVaults/vault-interaction.js +1894 -426
  108. package/build/exponentVaults/vault-interaction.js.map +1 -1
  109. package/build/exponentVaults/vault-interaction.kamino-vault.test.d.ts +1 -0
  110. package/build/exponentVaults/vault-interaction.kamino-vault.test.js +143 -0
  111. package/build/exponentVaults/vault-interaction.kamino-vault.test.js.map +1 -0
  112. package/build/exponentVaults/vault.d.ts +51 -2
  113. package/build/exponentVaults/vault.js +324 -48
  114. package/build/exponentVaults/vault.js.map +1 -1
  115. package/build/exponentVaults/vaultTransactionBuilder.d.ts +100 -134
  116. package/build/exponentVaults/vaultTransactionBuilder.js +383 -285
  117. package/build/exponentVaults/vaultTransactionBuilder.js.map +1 -1
  118. package/build/exponentVaults/vaultTransactionBuilder.test.d.ts +1 -0
  119. package/build/exponentVaults/vaultTransactionBuilder.test.js +297 -0
  120. package/build/exponentVaults/vaultTransactionBuilder.test.js.map +1 -0
  121. package/build/marketThree.d.ts +6 -2
  122. package/build/marketThree.js +10 -8
  123. package/build/marketThree.js.map +1 -1
  124. package/package.json +34 -32
  125. package/src/client/vaults/index.ts +2 -0
  126. package/src/client/vaults/types/index.ts +2 -0
  127. package/src/client/vaults/types/kaminoFarmEntry.ts +32 -0
  128. package/src/client/vaults/types/kaminoObligationEntry.ts +6 -3
  129. package/src/client/vaults/types/positionUpdate.ts +62 -0
  130. package/src/client/vaults/types/proposalAction.ts +0 -3
  131. package/src/client/vaults/types/reserveFarmMapping.ts +35 -0
  132. package/src/client/vaults/types/strategyPosition.ts +18 -1
  133. package/src/exponentVaults/aumCalculator.ts +353 -16
  134. package/src/exponentVaults/fetcher.ts +257 -0
  135. package/src/exponentVaults/index.ts +65 -40
  136. package/src/exponentVaults/kamino-farms.ts +538 -0
  137. package/src/exponentVaults/loopscale/client.ts +808 -0
  138. package/src/exponentVaults/loopscale/helpers.ts +172 -0
  139. package/src/exponentVaults/loopscale/index.ts +57 -0
  140. package/src/exponentVaults/loopscale/prepared-transactions.ts +435 -0
  141. package/src/exponentVaults/loopscale/prepared-types.ts +73 -0
  142. package/src/exponentVaults/loopscale/types.ts +466 -0
  143. package/src/exponentVaults/policyBuilders.ts +170 -0
  144. package/src/exponentVaults/pricePathResolver.test.ts +466 -0
  145. package/src/exponentVaults/pricePathResolver.ts +273 -0
  146. package/src/exponentVaults/syncTransaction.ts +6 -1
  147. package/src/exponentVaults/titan-quote.ts +231 -45
  148. package/src/exponentVaults/vault-instruction-types.ts +493 -0
  149. package/src/exponentVaults/vault-interaction.kamino-vault.test.ts +149 -0
  150. package/src/exponentVaults/vault-interaction.ts +2818 -799
  151. package/src/exponentVaults/vault.ts +474 -63
  152. package/src/exponentVaults/vaultTransactionBuilder.test.ts +349 -0
  153. package/src/exponentVaults/vaultTransactionBuilder.ts +581 -433
  154. package/src/marketThree.ts +14 -6
  155. package/src/exponentVaults/loopscale-client.ts +0 -1373
@@ -1,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 BS_AUTH = new web3_js_1.PublicKey("CyNKPfqsSLAejjZtEeNG3pR4SkPhSPHXdGhuNTyudrNs");
12
- const ASSOCIATED_TOKEN_PROGRAM_ID = new web3_js_1.PublicKey("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
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 integer`);
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 isEmptySignature(sig) {
53
- return sig.every((b) => b === 0);
27
+ function hasSignature(signature) {
28
+ return signature.some((value) => value !== 0);
54
29
  }
55
- function identifyInstruction(ix) {
56
- if (!ix.programId.equals(policyBuilders_1.LOOPSCALE_PROGRAM_ID))
57
- return null;
58
- const hex = Buffer.from(ix.data).subarray(0, 8).toString("hex");
59
- return DISCRIMINATOR_HEX_TO_NAME.get(hex) ?? null;
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 decompileInstructions(connection, transaction) {
62
- const lookupTableAccounts = await Promise.all(transaction.message.addressTableLookups.map(async (lookup) => {
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 buildTxFromLegacyEntry(entry) {
72
- const message = web3_js_1.VersionedMessage.deserialize(Buffer.from(entry.message, "base64"));
73
- const tx = new web3_js_1.VersionedTransaction(message);
74
- const signerKeys = message.staticAccountKeys.slice(0, message.header.numRequiredSignatures);
75
- for (const sig of entry.signatures) {
76
- const pubkey = new web3_js_1.PublicKey(sig.publicKey);
77
- const sigBytes = Buffer.from(sig.signature, "base64");
78
- if (sigBytes.length !== 64)
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 tx;
91
+ return null;
85
92
  }
86
- function mergeInputSignatures(resultTx, inputTx) {
87
- const signerKeys = resultTx.message.staticAccountKeys.slice(0, resultTx.message.header.numRequiredSignatures);
88
- const inputKeys = inputTx.message.staticAccountKeys;
89
- const inputNumSigners = inputTx.message.header.numRequiredSignatures;
90
- for (let i = 0; i < inputNumSigners; i++) {
91
- const inputSig = inputTx.signatures[i];
92
- if (isEmptySignature(inputSig))
93
- continue;
94
- const resultIdx = signerKeys.findIndex((key) => key.equals(inputKeys[i]));
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
- return resultTx;
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
- function ensureMessageMatches(a, b) {
104
- const am = Buffer.from(a.message.serialize());
105
- const bm = Buffer.from(b.message.serialize());
106
- if (!am.equals(bm)) {
107
- throw new Error("Loopscale returned a different transaction message; refusing to merge");
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
- // LoopscaleClient
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
- bsAuth;
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.bsAuth = config.bsAuth ?? BS_AUTH;
126
- this.useMpcCoSign = config.useMpcCoSign ?? true;
188
+ this.payer = toBase58(config.payer ?? config.userWallet);
127
189
  this.debug = config.debug ?? false;
128
190
  }
129
- // ── Vault execution context ──
130
- setVaultContext(ctx) {
131
- this.vaultCtx = ctx;
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
- return this.post("/markets/quote", body, { "user-wallet": this.userWallet });
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
- const body = {
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((f) => ({
267
- amount: toSafeNumber(f.amount, "amount"),
268
- assetData: f.assetData,
218
+ collateralFilter: params.collateralFilter.map((item) => ({
219
+ amount: toSafeNumber(item.amount, "amount"),
220
+ assetData: item.assetData,
269
221
  })),
270
- };
271
- return this.post("/markets/quote/max", body, { "user-wallet": this.userWallet });
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, { "user-wallet": this.userWallet });
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, { "user-wallet": this.userWallet });
278
- }
279
- async getCollateralHolders(params) {
280
- return this.post("/markets/collateral/holders", params);
236
+ return this.post("/markets/strategy/infos", params);
281
237
  }
282
- async getHistoricalCollateralHolders(params) {
283
- return this.post("/markets/collateral/holders/historical", params);
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
- const entries = await this.post("/markets/strategy/create", body, { payer: this.userWallet });
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
- const body = {
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
- payer: this.userWallet,
345
- });
274
+ }, this.payer));
346
275
  }
276
+ /**
277
+ * Build a raw Loopscale `withdrawStrategy` transaction.
278
+ */
347
279
  async withdrawStrategy(params) {
348
- const body = {
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
- payer: this.userWallet,
356
- });
286
+ }, this.payer));
357
287
  }
288
+ /**
289
+ * Build a raw Loopscale `closeStrategy` transaction.
290
+ */
358
291
  async closeStrategy(params) {
359
- const strategy = toBase58(params.strategy);
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 = { strategy: toBase58(params.strategy) };
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
- const entries = await this.post("/markets/strategy/update", body, { "User-Wallet": this.userWallet, payer: this.userWallet });
371
- if (!Array.isArray(entries) || entries.length === 0) {
372
- throw new Error("Loopscale strategy/update returned no transactions");
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 standalone borrow_principal wrapped as a LoopscaleInstruction for sync tx.
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((c) => ({
482
- collateralAmount: toSafeNumber(c.collateralAmount, "collateralAmount"),
483
- collateralAssetData: c.collateralAssetData,
315
+ depositCollateral: params.depositCollateral.map((item) => ({
316
+ collateralAmount: toSafeNumber(item.collateralAmount, "collateralAmount"),
317
+ collateralAssetData: item.collateralAssetData,
484
318
  })),
485
- principalRequested: params.principalRequested.map((p) => ({
486
- ledgerIndex: p.ledgerIndex,
487
- principalAmount: toSafeNumber(p.principalAmount, "principalAmount"),
488
- principalMint: toBase58(p.principalMint),
489
- strategy: toBase58(p.strategy),
490
- durationIndex: p.durationIndex,
491
- expectedLoanValues: p.expectedLoanValues ?? { expectedApy: 0, expectedLqt: [0, 0, 0, 0, 0] },
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
- const headers = { payer: this.userWallet };
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
- async apiBorrowPrincipal(params) {
514
- const borrowParams = {
515
- amount: toSafeNumber(params.borrowParams.amount, "amount"),
516
- duration: params.borrowParams.durationIndex,
517
- };
518
- if (params.borrowParams.expectedLoanValues) {
519
- borrowParams.expectedLoanValues = params.borrowParams.expectedLoanValues;
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.fetchAndClassifySingleTx("/markets/creditbook/borrow", body, { payer: this.userWallet });
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.fetchAndClassifySingleTx("/markets/creditbook/collateral/deposit", body, {
380
+ return this.post("/markets/creditbook/collateral/deposit", body, withPayerHeader({
545
381
  "user-wallet": this.userWallet,
546
- payer: this.userWallet,
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.fetchAndClassifySingleTx("/markets/creditbook/collateral/withdraw", body, {
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
- // ── Co-signing ──
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 serializedTx = Buffer.from(transaction.serialize()).toString("base64");
579
- const serializedMsg = Buffer.from(transaction.message.serialize()).toString("base64");
580
- if (this.useMpcCoSign) {
581
- const body = {
582
- batches: [{
583
- transactions: [{
584
- identifier: "loopscale-cosign",
585
- transaction: serializedTx,
586
- transactionType: 1,
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
- buildRawBundle(transaction, instructions, loopscaleInstructions) {
642
- return { transaction, instructions, loopscaleInstructions };
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
- return this.toSingleTxResult(batch);
429
+ const coSignedTransaction = web3_js_1.VersionedTransaction.deserialize(Buffer.from(transactionEntry.transaction, "base64"));
430
+ return mergeCoSignedTransaction(transaction, coSignedTransaction);
681
431
  }
682
- async fetchAndClassifySingleTx(path, body, headers) {
683
- const entry = await this.post(path, body, headers);
684
- const batch = await this.parseTransactionEntry(entry);
685
- if (this.debug) {
686
- console.log(`[LoopscaleClient] ${path}: syncActions=${batch.syncActions.length}, topLevel=${batch.topLevelInstructions.length}`);
687
- }
688
- return this.toSingleTxResult(batch);
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 delay = 2000 * (attempt + 1);
711
- if (this.debug)
712
- console.log(` RATE LIMITED (429), retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})...`);
713
- await new Promise((r) => setTimeout(r, delay));
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.ok) {
719
- const text = await response.text();
720
- if (this.debug)
721
- console.log(` ERROR ${response.status}: ${text}`);
722
- throw new Error(`Loopscale API ${path} failed (${response.status}): ${text}`);
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) {