@hobba-io/core 0.1.0

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 (190) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +87 -0
  3. package/dist/amountUtils.d.ts +15 -0
  4. package/dist/amountUtils.d.ts.map +1 -0
  5. package/dist/amountUtils.js +59 -0
  6. package/dist/amountUtils.js.map +1 -0
  7. package/dist/constants.d.ts +148 -0
  8. package/dist/constants.d.ts.map +1 -0
  9. package/dist/constants.js +231 -0
  10. package/dist/constants.js.map +1 -0
  11. package/dist/errors.d.ts +7 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +29 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/executeAction.d.ts +36 -0
  16. package/dist/executeAction.d.ts.map +1 -0
  17. package/dist/executeAction.js +606 -0
  18. package/dist/executeAction.js.map +1 -0
  19. package/dist/executeDepositBorrow.d.ts +49 -0
  20. package/dist/executeDepositBorrow.d.ts.map +1 -0
  21. package/dist/executeDepositBorrow.js +444 -0
  22. package/dist/executeDepositBorrow.js.map +1 -0
  23. package/dist/idl/hobba.json +5072 -0
  24. package/dist/idl.d.ts +783 -0
  25. package/dist/idl.d.ts.map +1 -0
  26. package/dist/idl.js +7 -0
  27. package/dist/idl.js.map +1 -0
  28. package/dist/index.d.ts +17 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +22 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/juplend/accountResolvers.d.ts +106 -0
  33. package/dist/juplend/accountResolvers.d.ts.map +1 -0
  34. package/dist/juplend/accountResolvers.js +148 -0
  35. package/dist/juplend/accountResolvers.js.map +1 -0
  36. package/dist/juplend/operateRemaining.d.ts +11 -0
  37. package/dist/juplend/operateRemaining.d.ts.map +1 -0
  38. package/dist/juplend/operateRemaining.js +46 -0
  39. package/dist/juplend/operateRemaining.js.map +1 -0
  40. package/dist/juplend/positionInit.d.ts +34 -0
  41. package/dist/juplend/positionInit.d.ts.map +1 -0
  42. package/dist/juplend/positionInit.js +56 -0
  43. package/dist/juplend/positionInit.js.map +1 -0
  44. package/dist/juplend/positionParser.d.ts +8 -0
  45. package/dist/juplend/positionParser.d.ts.map +1 -0
  46. package/dist/juplend/positionParser.js +30 -0
  47. package/dist/juplend/positionParser.js.map +1 -0
  48. package/dist/juplend/sdkApi.d.ts +39 -0
  49. package/dist/juplend/sdkApi.d.ts.map +1 -0
  50. package/dist/juplend/sdkApi.js +68 -0
  51. package/dist/juplend/sdkApi.js.map +1 -0
  52. package/dist/juplend/tickMath.d.ts +19 -0
  53. package/dist/juplend/tickMath.d.ts.map +1 -0
  54. package/dist/juplend/tickMath.js +91 -0
  55. package/dist/juplend/tickMath.js.map +1 -0
  56. package/dist/juplend/tickUtils.d.ts +20 -0
  57. package/dist/juplend/tickUtils.d.ts.map +1 -0
  58. package/dist/juplend/tickUtils.js +115 -0
  59. package/dist/juplend/tickUtils.js.map +1 -0
  60. package/dist/juplend/vaultConfigParser.d.ts +25 -0
  61. package/dist/juplend/vaultConfigParser.d.ts.map +1 -0
  62. package/dist/juplend/vaultConfigParser.js +38 -0
  63. package/dist/juplend/vaultConfigParser.js.map +1 -0
  64. package/dist/kamino/accountResolvers.d.ts +110 -0
  65. package/dist/kamino/accountResolvers.d.ts.map +1 -0
  66. package/dist/kamino/accountResolvers.js +220 -0
  67. package/dist/kamino/accountResolvers.js.map +1 -0
  68. package/dist/kamino/obligationParser.d.ts +13 -0
  69. package/dist/kamino/obligationParser.d.ts.map +1 -0
  70. package/dist/kamino/obligationParser.js +92 -0
  71. package/dist/kamino/obligationParser.js.map +1 -0
  72. package/dist/kamino/refreshInstructions.d.ts +4 -0
  73. package/dist/kamino/refreshInstructions.d.ts.map +1 -0
  74. package/dist/kamino/refreshInstructions.js +32 -0
  75. package/dist/kamino/refreshInstructions.js.map +1 -0
  76. package/dist/limits.d.ts +87 -0
  77. package/dist/limits.d.ts.map +1 -0
  78. package/dist/limits.js +174 -0
  79. package/dist/limits.js.map +1 -0
  80. package/dist/marketConfig.d.ts +35 -0
  81. package/dist/marketConfig.d.ts.map +1 -0
  82. package/dist/marketConfig.js +85 -0
  83. package/dist/marketConfig.js.map +1 -0
  84. package/dist/oracle/scopePrice.d.ts +7 -0
  85. package/dist/oracle/scopePrice.d.ts.map +1 -0
  86. package/dist/oracle/scopePrice.js +31 -0
  87. package/dist/oracle/scopePrice.js.map +1 -0
  88. package/dist/pdas.d.ts +33 -0
  89. package/dist/pdas.d.ts.map +1 -0
  90. package/dist/pdas.js +116 -0
  91. package/dist/pdas.js.map +1 -0
  92. package/dist/position.d.ts +45 -0
  93. package/dist/position.d.ts.map +1 -0
  94. package/dist/position.js +68 -0
  95. package/dist/position.js.map +1 -0
  96. package/dist/positionMetrics.d.ts +108 -0
  97. package/dist/positionMetrics.d.ts.map +1 -0
  98. package/dist/positionMetrics.js +238 -0
  99. package/dist/positionMetrics.js.map +1 -0
  100. package/dist/program.d.ts +12 -0
  101. package/dist/program.d.ts.map +1 -0
  102. package/dist/program.js +24 -0
  103. package/dist/program.js.map +1 -0
  104. package/dist/quote.d.ts +90 -0
  105. package/dist/quote.d.ts.map +1 -0
  106. package/dist/quote.js +198 -0
  107. package/dist/quote.js.map +1 -0
  108. package/dist/rates/borrowRates.d.ts +44 -0
  109. package/dist/rates/borrowRates.d.ts.map +1 -0
  110. package/dist/rates/borrowRates.js +229 -0
  111. package/dist/rates/borrowRates.js.map +1 -0
  112. package/dist/rates/collateralInfo.d.ts +21 -0
  113. package/dist/rates/collateralInfo.d.ts.map +1 -0
  114. package/dist/rates/collateralInfo.js +104 -0
  115. package/dist/rates/collateralInfo.js.map +1 -0
  116. package/dist/rates/constants.d.ts +22 -0
  117. package/dist/rates/constants.d.ts.map +1 -0
  118. package/dist/rates/constants.js +70 -0
  119. package/dist/rates/constants.js.map +1 -0
  120. package/dist/rates/index.d.ts +11 -0
  121. package/dist/rates/index.d.ts.map +1 -0
  122. package/dist/rates/index.js +11 -0
  123. package/dist/rates/index.js.map +1 -0
  124. package/dist/rates/kaminoApi.d.ts +24 -0
  125. package/dist/rates/kaminoApi.d.ts.map +1 -0
  126. package/dist/rates/kaminoApi.js +98 -0
  127. package/dist/rates/kaminoApi.js.map +1 -0
  128. package/dist/rates/supplyRates.d.ts +23 -0
  129. package/dist/rates/supplyRates.d.ts.map +1 -0
  130. package/dist/rates/supplyRates.js +67 -0
  131. package/dist/rates/supplyRates.js.map +1 -0
  132. package/dist/transactionBuilder.d.ts +5 -0
  133. package/dist/transactionBuilder.d.ts.map +1 -0
  134. package/dist/transactionBuilder.js +287 -0
  135. package/dist/transactionBuilder.js.map +1 -0
  136. package/dist/txHelpers.d.ts +16 -0
  137. package/dist/txHelpers.d.ts.map +1 -0
  138. package/dist/txHelpers.js +82 -0
  139. package/dist/txHelpers.js.map +1 -0
  140. package/dist/userState.d.ts +21 -0
  141. package/dist/userState.d.ts.map +1 -0
  142. package/dist/userState.js +60 -0
  143. package/dist/userState.js.map +1 -0
  144. package/dist/vault/vaultAccounts.d.ts +82 -0
  145. package/dist/vault/vaultAccounts.d.ts.map +1 -0
  146. package/dist/vault/vaultAccounts.js +275 -0
  147. package/dist/vault/vaultAccounts.js.map +1 -0
  148. package/dist/wallet.d.ts +12 -0
  149. package/dist/wallet.d.ts.map +1 -0
  150. package/dist/wallet.js +2 -0
  151. package/dist/wallet.js.map +1 -0
  152. package/package.json +63 -0
  153. package/src/amountUtils.ts +69 -0
  154. package/src/constants.ts +263 -0
  155. package/src/errors.ts +33 -0
  156. package/src/executeAction.ts +884 -0
  157. package/src/executeDepositBorrow.ts +677 -0
  158. package/src/idl/hobba.json +5072 -0
  159. package/src/idl.ts +7 -0
  160. package/src/index.ts +21 -0
  161. package/src/juplend/accountResolvers.ts +207 -0
  162. package/src/juplend/operateRemaining.ts +81 -0
  163. package/src/juplend/positionInit.ts +86 -0
  164. package/src/juplend/positionParser.ts +41 -0
  165. package/src/juplend/sdkApi.ts +112 -0
  166. package/src/juplend/tickMath.ts +82 -0
  167. package/src/juplend/tickUtils.ts +163 -0
  168. package/src/juplend/vaultConfigParser.ts +45 -0
  169. package/src/kamino/accountResolvers.ts +299 -0
  170. package/src/kamino/obligationParser.ts +123 -0
  171. package/src/kamino/refreshInstructions.ts +45 -0
  172. package/src/limits.ts +222 -0
  173. package/src/marketConfig.ts +139 -0
  174. package/src/oracle/scopePrice.ts +31 -0
  175. package/src/pdas.ts +226 -0
  176. package/src/position.ts +131 -0
  177. package/src/positionMetrics.ts +387 -0
  178. package/src/program.ts +30 -0
  179. package/src/quote.ts +310 -0
  180. package/src/rates/borrowRates.ts +270 -0
  181. package/src/rates/collateralInfo.ts +136 -0
  182. package/src/rates/constants.ts +87 -0
  183. package/src/rates/index.ts +38 -0
  184. package/src/rates/kaminoApi.ts +134 -0
  185. package/src/rates/supplyRates.ts +102 -0
  186. package/src/transactionBuilder.ts +324 -0
  187. package/src/txHelpers.ts +107 -0
  188. package/src/userState.ts +91 -0
  189. package/src/vault/vaultAccounts.ts +365 -0
  190. package/src/wallet.ts +12 -0
@@ -0,0 +1,324 @@
1
+ import {
2
+ Connection,
3
+ TransactionInstruction,
4
+ Transaction,
5
+ TransactionMessage,
6
+ VersionedTransaction,
7
+ ComputeBudgetProgram,
8
+ PublicKey,
9
+ AddressLookupTableAccount,
10
+ } from "@solana/web3.js";
11
+ import { isTickError } from "./juplend/tickUtils";
12
+ import type { HobbaSigner } from "./wallet";
13
+
14
+ /** Poll-based confirmation — avoids WebSocket dependency */
15
+ async function pollConfirmation(
16
+ connection: Connection,
17
+ signature: string,
18
+ lastValidBlockHeight: number,
19
+ timeoutMs: number = 60_000
20
+ ): Promise<void> {
21
+ const start = Date.now();
22
+ while (Date.now() - start < timeoutMs) {
23
+ try {
24
+ const { value } = await connection.getSignatureStatuses([signature]);
25
+ const status = value[0];
26
+ if (status) {
27
+ if (status.err) throw new Error(`Transaction failed: ${JSON.stringify(status.err)}`);
28
+ if (status.confirmationStatus === "confirmed" || status.confirmationStatus === "finalized") return;
29
+ }
30
+ } catch (e: any) {
31
+ // Ignore RPC errors (429 etc.) and keep polling
32
+ console.warn("[pollConfirmation] RPC error, retrying:", e.message?.slice(0, 80));
33
+ }
34
+ await new Promise((r) => setTimeout(r, 2_000));
35
+ }
36
+ throw new Error(`Transaction not confirmed within ${timeoutMs / 1000}s. Check: https://solscan.io/tx/${signature}`);
37
+ }
38
+
39
+ export async function sendLegacyTx(
40
+ connection: Connection,
41
+ instructions: TransactionInstruction[],
42
+ wallet: HobbaSigner,
43
+ computeUnits?: number
44
+ ): Promise<string> {
45
+ if (!wallet.publicKey || !wallet.signTransaction) {
46
+ throw new Error("Wallet not connected");
47
+ }
48
+
49
+ const tx = new Transaction();
50
+
51
+ if (computeUnits) {
52
+ tx.add(ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnits }));
53
+ }
54
+ tx.add(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 100_000 }));
55
+
56
+ for (const ix of instructions) {
57
+ tx.add(ix);
58
+ }
59
+
60
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
61
+ tx.recentBlockhash = blockhash;
62
+ tx.feePayer = wallet.publicKey;
63
+
64
+ const signed = await wallet.signTransaction(tx);
65
+ const sig = await connection.sendRawTransaction(signed.serialize(), {
66
+ skipPreflight: false,
67
+ });
68
+
69
+ await pollConfirmation(connection, sig, lastValidBlockHeight);
70
+ return sig;
71
+ }
72
+
73
+ export async function sendVersionedTx(
74
+ connection: Connection,
75
+ instructions: TransactionInstruction[],
76
+ wallet: HobbaSigner,
77
+ lutAddresses: PublicKey[],
78
+ computeUnits?: number,
79
+ skipSimulation?: boolean,
80
+ // If supplied, called when pre-sign simulation fails with a JupLend tick error
81
+ // (VaultInvalidTick / VaultTickMismatch / VaultTickHasDebtNotFound). Should return
82
+ // a fresh instruction list (typically by re-fetching the SDK operate context) or
83
+ // null to surface the original error.
84
+ rebuildOnTickError?: () => Promise<TransactionInstruction[] | null>
85
+ ): Promise<string> {
86
+ if (!wallet.publicKey || !wallet.signTransaction) {
87
+ throw new Error("Wallet not connected");
88
+ }
89
+
90
+ const budgetIxs = [
91
+ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 100_000 }),
92
+ ...(computeUnits ? [ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnits })] : []),
93
+ ];
94
+ const allInstructions = [...budgetIxs, ...instructions];
95
+
96
+ // Load lookup tables
97
+ const lookupTables: AddressLookupTableAccount[] = [];
98
+ for (const lutAddress of lutAddresses) {
99
+ try {
100
+ const lutAccount = await connection.getAddressLookupTable(lutAddress);
101
+ if (lutAccount.value) {
102
+ lookupTables.push(lutAccount.value);
103
+ } else {
104
+ console.warn(`[tx] LUT ${lutAddress.toBase58()} returned null (not found on RPC)`);
105
+ }
106
+ } catch (e: any) {
107
+ console.warn(`[tx] LUT ${lutAddress.toBase58()} fetch failed: ${e?.message || e}`);
108
+ }
109
+ }
110
+
111
+ if (lookupTables.length === 0 && lutAddresses.length > 0) {
112
+ throw new Error("LUT not found or not yet activated");
113
+ }
114
+
115
+ const INIT_TICK_DISC = "160d3e8d4959b21d";
116
+ const INIT_TICK_ID_LIQ_DISC = "386e79a998f156b7";
117
+ const JUPLEND_PROGRAM = "jupr81YtYssSyPt8jbnGuiWon5f6x9TcDEFxYe3Bdzi";
118
+ const MAX_TX_SIZE = 1232;
119
+
120
+ const isInitTickIx = (ix: TransactionInstruction) => {
121
+ if (ix.programId.toBase58() !== JUPLEND_PROGRAM) return false;
122
+ const disc = Buffer.from(ix.data).subarray(0, 8).toString("hex");
123
+ return disc === INIT_TICK_DISC || disc === INIT_TICK_ID_LIQ_DISC;
124
+ };
125
+
126
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
127
+ let finalInstructions = allInstructions;
128
+
129
+ const tryBuild = (ixs: TransactionInstruction[]): { tx: VersionedTransaction; size: number } | null => {
130
+ try {
131
+ const msg = new TransactionMessage({
132
+ payerKey: wallet.publicKey!,
133
+ recentBlockhash: blockhash,
134
+ instructions: ixs,
135
+ }).compileToV0Message(lookupTables);
136
+ const tx = new VersionedTransaction(msg);
137
+ const size = tx.serialize().length;
138
+ return { tx, size };
139
+ } catch (e: any) {
140
+ if (String(e?.message || e).includes("overruns Uint8Array")) return null;
141
+ throw e;
142
+ }
143
+ };
144
+
145
+ let built = tryBuild(finalInstructions);
146
+ // If TX doesn't fit (build threw or over MAX_TX_SIZE), strip InitTick/InitTickIdLiq (retry path re-adds if simulation says missing)
147
+ if (!built || built.size > MAX_TX_SIZE) {
148
+ const stripped = finalInstructions.filter(ix => !isInitTickIx(ix));
149
+ if (stripped.length < finalInstructions.length) {
150
+ console.log(`[tx] TX too large (${built?.size ?? "overflow"}), stripping ${finalInstructions.length - stripped.length} InitTick instructions`);
151
+ finalInstructions = stripped;
152
+ built = tryBuild(finalInstructions);
153
+ }
154
+ }
155
+ if (!built) {
156
+ throw new Error(`Transaction too large even after stripping InitTick. Try splitting deposit/borrow or contact support.`);
157
+ }
158
+ const versionedTx = built.tx;
159
+
160
+ // Pre-simulate to catch errors before opening wallet popup
161
+ let txToSign = versionedTx;
162
+ if (skipSimulation) {
163
+ const signed = await wallet.signTransaction(versionedTx);
164
+ const sig = await connection.sendRawTransaction(signed.serialize(), { skipPreflight: true });
165
+ await pollConfirmation(connection, sig, lastValidBlockHeight);
166
+ return sig;
167
+ }
168
+ const simResult = await connection.simulateTransaction(versionedTx, {
169
+ sigVerify: false,
170
+ replaceRecentBlockhash: true,
171
+ });
172
+ if (simResult.value.err) {
173
+ if (simResult.value.logs) {
174
+ console.log("[simulation] Full logs:");
175
+ simResult.value.logs.forEach((l, i) => console.log(` ${i}: ${l}`));
176
+ }
177
+
178
+ const logs = simResult.value.logs || [];
179
+ const logBlob = logs.join("\n");
180
+
181
+ // JupLend tick race: SDK-computed tick PDAs no longer match on-chain state.
182
+ // Recover by re-fetching the SDK operate context and rebuilding the ix list,
183
+ // then re-simulating with fresh ticks. Single wallet popup at the end.
184
+ // Retry up to 3 times — vault state can shift again between SDK refetch and
185
+ // the rebuilt simulation, especially for withdraw paths that touch tick_has_debt arrays.
186
+ let tickRebuildSucceeded = false;
187
+ if (rebuildOnTickError && isTickError(logBlob)) {
188
+ const MAX_REBUILD_ATTEMPTS = 3;
189
+ let lastErrJson = "";
190
+ let lastLogs = "";
191
+ for (let attempt = 0; attempt < MAX_REBUILD_ATTEMPTS && !tickRebuildSucceeded; attempt++) {
192
+ console.log(`[simulation] JupLend tick error — rebuilding from fresh SDK context (attempt ${attempt + 1}/${MAX_REBUILD_ATTEMPTS})`);
193
+ const rebuilt = await rebuildOnTickError();
194
+ if (!rebuilt) break;
195
+ let rebuiltIxs = [...budgetIxs, ...rebuilt];
196
+ // Strip any init-tick ixs that won't fit; the SDK already filters missing ones
197
+ let candidate = tryBuild(rebuiltIxs);
198
+ if (!candidate || candidate.size > MAX_TX_SIZE) {
199
+ const stripped = rebuiltIxs.filter(ix => !isInitTickIx(ix));
200
+ candidate = tryBuild(stripped);
201
+ if (candidate) rebuiltIxs = stripped;
202
+ }
203
+ if (!candidate) break;
204
+ const retrySim = await connection.simulateTransaction(candidate.tx, {
205
+ sigVerify: false,
206
+ replaceRecentBlockhash: true,
207
+ });
208
+ if (!retrySim.value.err) {
209
+ finalInstructions = rebuiltIxs;
210
+ txToSign = candidate.tx;
211
+ tickRebuildSucceeded = true;
212
+ break;
213
+ }
214
+ lastErrJson = JSON.stringify(retrySim.value.err);
215
+ lastLogs = retrySim.value.logs?.slice(-10).join("\n") || "";
216
+ const retryBlob = (retrySim.value.logs || []).join("\n");
217
+ // If the new error isn't a tick error, no point retrying
218
+ if (!isTickError(retryBlob)) {
219
+ throw new Error(`Simulation failed after tick rebuild: ${lastErrJson}\n${lastLogs}`);
220
+ }
221
+ }
222
+ if (!tickRebuildSucceeded) {
223
+ throw new Error(`Simulation failed after ${MAX_REBUILD_ATTEMPTS} tick rebuild attempts: ${lastErrJson}\n${lastLogs}`);
224
+ }
225
+ }
226
+
227
+ if (tickRebuildSucceeded) {
228
+ // fall through to signing below with the rebuilt instructions
229
+ } else {
230
+ // If an InitTick/InitTickIdLiquidation failed (already exists, or unrecognized discriminator),
231
+ // strip those instructions and retry the simulation.
232
+ const hasAlreadyInUse = logs.some(l => l.includes("already in use"));
233
+ const hasFallbackNotFound = logs.some(l =>
234
+ l.includes("InstructionFallbackNotFound") || l.includes("custom program error: 0x65")
235
+ );
236
+ if (hasAlreadyInUse || hasFallbackNotFound) {
237
+ // Strip problematic JupLend instructions:
238
+ // - "already in use": InitTick accounts already exist
239
+ // - "FallbackNotFound": SDK generated an instruction the program doesn't recognize
240
+ const filtered = finalInstructions.filter(ix => {
241
+ if (ix.programId.toBase58() !== JUPLEND_PROGRAM) return true;
242
+ // Keep known-good InitTick instructions, strip everything else from JupLend
243
+ if (isInitTickIx(ix)) {
244
+ // Strip InitTick only if accounts already exist
245
+ return !hasAlreadyInUse;
246
+ }
247
+ // Strip unrecognized JupLend instructions (causes FallbackNotFound)
248
+ return !hasFallbackNotFound;
249
+ });
250
+ if (filtered.length < finalInstructions.length) {
251
+ console.log("[simulation] Retrying without initTick instructions");
252
+ finalInstructions = filtered;
253
+ const retryMsg = new TransactionMessage({
254
+ payerKey: wallet.publicKey,
255
+ recentBlockhash: blockhash,
256
+ instructions: finalInstructions,
257
+ }).compileToV0Message(lookupTables);
258
+ txToSign = new VersionedTransaction(retryMsg);
259
+ const retryResult = await connection.simulateTransaction(txToSign, {
260
+ sigVerify: false,
261
+ replaceRecentBlockhash: true,
262
+ });
263
+ if (retryResult.value.err) {
264
+ const retryLogs = retryResult.value.logs?.slice(-10).join("\n") || "";
265
+ throw new Error(`Simulation failed: ${JSON.stringify(retryResult.value.err)}\n${retryLogs}`);
266
+ }
267
+ } else {
268
+ const logStr = logs.slice(-10).join("\n");
269
+ throw new Error(`Simulation failed: ${JSON.stringify(simResult.value.err)}\n${logStr}`);
270
+ }
271
+ } else {
272
+ const logStr = logs.slice(-10).join("\n");
273
+ throw new Error(`Simulation failed: ${JSON.stringify(simResult.value.err)}\n${logStr}`);
274
+ }
275
+ } // close tickRebuildSucceeded else
276
+ }
277
+
278
+ // Rebuild with fresh blockhash — the original may be stale after simulation + wallet delay.
279
+ // Extract instructions from the compiled message and recompile with fresh blockhash.
280
+ const { blockhash: freshHash } = await connection.getLatestBlockhash();
281
+ const freshMsg = new TransactionMessage({
282
+ payerKey: wallet.publicKey,
283
+ recentBlockhash: freshHash,
284
+ instructions: txToSign === versionedTx ? finalInstructions : finalInstructions,
285
+ }).compileToV0Message(lookupTables);
286
+ const freshTx = new VersionedTransaction(freshMsg);
287
+
288
+ const signed = await wallet.signTransaction(freshTx);
289
+ const rawTx = signed.serialize();
290
+
291
+ const sig = await connection.sendRawTransaction(rawTx, {
292
+ skipPreflight: true,
293
+ });
294
+
295
+ // Poll for confirmation, re-sending periodically in case the TX was dropped
296
+ const start = Date.now();
297
+ const TIMEOUT_MS = 60_000;
298
+ const RESEND_INTERVAL_MS = 4_000;
299
+ let lastResend = Date.now();
300
+
301
+ while (Date.now() - start < TIMEOUT_MS) {
302
+ try {
303
+ const { value } = await connection.getSignatureStatuses([sig]);
304
+ const status = value[0];
305
+ if (status) {
306
+ if (status.err) throw new Error(`Transaction failed: ${JSON.stringify(status.err)}`);
307
+ if (status.confirmationStatus === "confirmed" || status.confirmationStatus === "finalized") return sig;
308
+ }
309
+ } catch (e: any) {
310
+ console.warn("[confirm] RPC error, retrying:", e.message?.slice(0, 80));
311
+ }
312
+
313
+ // Re-send TX periodically in case it was dropped
314
+ if (Date.now() - lastResend > RESEND_INTERVAL_MS) {
315
+ try {
316
+ await connection.sendRawTransaction(rawTx, { skipPreflight: true });
317
+ lastResend = Date.now();
318
+ } catch {}
319
+ }
320
+
321
+ await new Promise((r) => setTimeout(r, 2_000));
322
+ }
323
+ throw new Error(`Transaction not confirmed within ${TIMEOUT_MS / 1000}s. Check: https://solscan.io/tx/${sig}`);
324
+ }
@@ -0,0 +1,107 @@
1
+ import { Connection, PublicKey, SystemProgram, TransactionInstruction } from "@solana/web3.js";
2
+ import {
3
+ createAssociatedTokenAccountInstruction,
4
+ createSyncNativeInstruction,
5
+ NATIVE_MINT,
6
+ } from "@solana/spl-token";
7
+ import BN from "bn.js";
8
+ import type { deriveUserTokenAccounts } from "./pdas";
9
+ import type { VaultInfo } from "./vault/vaultAccounts";
10
+
11
+ export type UserTokenAccounts = Awaited<ReturnType<typeof deriveUserTokenAccounts>>;
12
+
13
+ export type SafeGetAccountInfo = (
14
+ addr: PublicKey,
15
+ ) => Promise<Awaited<ReturnType<Connection["getAccountInfo"]>>>;
16
+
17
+ export function makeSafeGetAccountInfo(connection: Connection): SafeGetAccountInfo {
18
+ return async (addr: PublicKey) => {
19
+ try {
20
+ return await connection.getAccountInfo(addr);
21
+ } catch {
22
+ return null;
23
+ }
24
+ };
25
+ }
26
+
27
+ /** Build wSOL wrapping instructions (create ATA if missing → transfer → sync). */
28
+ export async function buildWrapSolIxs(
29
+ owner: PublicKey,
30
+ wsolAta: PublicKey,
31
+ lamports: BN,
32
+ safeGetAccountInfo: SafeGetAccountInfo,
33
+ ): Promise<TransactionInstruction[]> {
34
+ const ixs: TransactionInstruction[] = [];
35
+ const ataInfo = await safeGetAccountInfo(wsolAta);
36
+ if (!ataInfo) {
37
+ ixs.push(createAssociatedTokenAccountInstruction(owner, wsolAta, owner, NATIVE_MINT));
38
+ }
39
+ ixs.push(
40
+ SystemProgram.transfer({
41
+ fromPubkey: owner,
42
+ toPubkey: wsolAta,
43
+ lamports: BigInt(lamports.toString()),
44
+ }),
45
+ createSyncNativeInstruction(wsolAta),
46
+ );
47
+ return ixs;
48
+ }
49
+
50
+ /** Shares ATA (owned by the UserState PDA) for the given yield vault. */
51
+ export function getSharesAtaForVault(vaultType: string, tokens: UserTokenAccounts): PublicKey {
52
+ switch (vaultType) {
53
+ case "prime":
54
+ return tokens.pdaPrimeSharesAta;
55
+ case "rockawayRWA":
56
+ return tokens.pdaRockawaySharesAta;
57
+ case "perena":
58
+ return tokens.pdaPerenaSharesAta;
59
+ default:
60
+ return tokens.pdaSharesAta; // allez
61
+ }
62
+ }
63
+
64
+ /** Fetch a LUT address from the Hobba backend; null when not configured. */
65
+ export async function fetchMarketLut(
66
+ apiBaseUrl: string,
67
+ market: string,
68
+ ): Promise<PublicKey | null> {
69
+ try {
70
+ const resp = await fetch(`${apiBaseUrl}/api/lut?market=${market}`);
71
+ const data = await resp.json();
72
+ return data.address ? new PublicKey(data.address) : null;
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ /** Resolve LUTs for a vault, fetching from the backend + per-vault constant. */
79
+ export async function resolveVaultLuts(
80
+ apiBaseUrl: string,
81
+ vi: VaultInfo,
82
+ extraLuts?: PublicKey[],
83
+ ): Promise<PublicKey[]> {
84
+ const luts: PublicKey[] = [];
85
+ const seen = new Set<string>();
86
+ const addLut = (pk: PublicKey) => {
87
+ const key = pk.toBase58();
88
+ if (!seen.has(key) && !pk.equals(PublicKey.default)) {
89
+ seen.add(key);
90
+ luts.push(pk);
91
+ }
92
+ };
93
+ if (vi.provider === "perena") {
94
+ const lut = await fetchMarketLut(apiBaseUrl, "perena");
95
+ if (lut) addLut(lut);
96
+ } else {
97
+ // Vault LUT from the backend (batch accounts for the kvault CPI) …
98
+ const lut = await fetchMarketLut(apiBaseUrl, "vault");
99
+ if (lut) addLut(lut);
100
+ // … plus the per-vault constant LUT as fallback.
101
+ if (!vi.lookupTable.equals(PublicKey.default)) addLut(vi.lookupTable);
102
+ }
103
+ if (extraLuts) {
104
+ for (const l of extraLuts) addLut(l);
105
+ }
106
+ return luts;
107
+ }
@@ -0,0 +1,91 @@
1
+ import { Connection, PublicKey } from "@solana/web3.js";
2
+ import BN from "bn.js";
3
+ import {
4
+ CBBTC_MINT,
5
+ DEPOSITED_TO_VAULT_OFFSET,
6
+ CURRENT_VAULT_OFFSET,
7
+ COLLATERAL_MINT_OFFSET,
8
+ ALLEZ_USDC_VAULT,
9
+ USDC_PRIME_VAULT,
10
+ ROCKAWAY_RWA_VAULT,
11
+ PERENA_VAULT_STATE,
12
+ } from "./constants";
13
+ import { deriveUserStatePda } from "./pdas";
14
+
15
+ export type CurrentVaultType = "allez" | "prime" | "rockawayRWA" | "perena" | "none";
16
+
17
+ export interface UserStateData {
18
+ owner: PublicKey;
19
+ obligation: PublicKey;
20
+ lendingMarket: PublicKey;
21
+ lendingProvider: number; // 0 = Kamino, 1 = JupLend
22
+ depositedToVault: number; // USDC lamports
23
+ currentVault: PublicKey;
24
+ currentVaultType: CurrentVaultType;
25
+ collateralMint: PublicKey;
26
+ }
27
+
28
+ /**
29
+ * Parse a Hobba UserState account buffer. Lifted verbatim from the app's
30
+ * useUserState hook so the app, the widget, and headless integrators share one
31
+ * layout definition.
32
+ */
33
+ export function parseUserStateData(data: Buffer): UserStateData {
34
+ const owner = new PublicKey(data.slice(9, 41));
35
+ const obligation = new PublicKey(data.slice(41, 73));
36
+ const lendingMarket = new PublicKey(data.slice(73, 105));
37
+ const lendingProvider = data[105];
38
+
39
+ let depositedToVault = 0;
40
+ if (data.length >= DEPOSITED_TO_VAULT_OFFSET + 8) {
41
+ depositedToVault = new BN(
42
+ data.slice(DEPOSITED_TO_VAULT_OFFSET, DEPOSITED_TO_VAULT_OFFSET + 8),
43
+ "le",
44
+ ).toNumber();
45
+ }
46
+
47
+ let currentVault = PublicKey.default;
48
+ let currentVaultType: CurrentVaultType = "none";
49
+ if (data.length >= CURRENT_VAULT_OFFSET + 32) {
50
+ currentVault = new PublicKey(data.slice(CURRENT_VAULT_OFFSET, CURRENT_VAULT_OFFSET + 32));
51
+ if (!currentVault.equals(PublicKey.default)) {
52
+ if (currentVault.equals(ALLEZ_USDC_VAULT)) currentVaultType = "allez";
53
+ else if (currentVault.equals(USDC_PRIME_VAULT)) currentVaultType = "prime";
54
+ else if (currentVault.equals(ROCKAWAY_RWA_VAULT)) currentVaultType = "rockawayRWA";
55
+ else if (currentVault.equals(PERENA_VAULT_STATE)) currentVaultType = "perena";
56
+ }
57
+ }
58
+
59
+ let collateralMint = CBBTC_MINT; // default for older accounts
60
+ if (data.length >= COLLATERAL_MINT_OFFSET + 32) {
61
+ collateralMint = new PublicKey(
62
+ data.slice(COLLATERAL_MINT_OFFSET, COLLATERAL_MINT_OFFSET + 32),
63
+ );
64
+ if (collateralMint.equals(PublicKey.default)) {
65
+ collateralMint = CBBTC_MINT;
66
+ }
67
+ }
68
+
69
+ return {
70
+ owner,
71
+ obligation,
72
+ lendingMarket,
73
+ lendingProvider,
74
+ depositedToVault,
75
+ currentVault,
76
+ currentVaultType,
77
+ collateralMint,
78
+ };
79
+ }
80
+
81
+ /** Fetch + parse the UserState for (owner, collateral mint). Null when none exists. */
82
+ export async function fetchUserState(
83
+ connection: Connection,
84
+ owner: PublicKey,
85
+ collateralMint: PublicKey,
86
+ ): Promise<UserStateData | null> {
87
+ const [pda] = deriveUserStatePda(owner, collateralMint);
88
+ const info = await connection.getAccountInfo(pda);
89
+ if (!info) return null;
90
+ return parseUserStateData(info.data as Buffer);
91
+ }