@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
@@ -0,0 +1,273 @@
1
+ import type { ExponentPrice, ExponentPrices } from "@exponent-labs/exponent-vaults-fetcher"
2
+ import { PublicKey } from "@solana/web3.js"
3
+ import { priceId, type PriceId } from "../client/vaults/types/priceId"
4
+
5
+ type PriceEdge = {
6
+ priceId: bigint
7
+ priceMint: PublicKey
8
+ underlyingMint: PublicKey
9
+ }
10
+
11
+ export type ResolvedPricePath = {
12
+ sourceMint: PublicKey
13
+ targetMint: PublicKey
14
+ edges: PriceEdge[]
15
+ mints: PublicKey[]
16
+ }
17
+
18
+ export type ResolvedKaminoQuotePath = {
19
+ quoteInputMint: PublicKey
20
+ quotePriceId: PriceId
21
+ }
22
+
23
+ export function resolveExponentPricePath(
24
+ prices: ExponentPrices,
25
+ sourceMint: PublicKey,
26
+ targetMint: PublicKey,
27
+ ): ResolvedPricePath | null {
28
+ const startKey = sourceMint.toBase58()
29
+ const targetKey = targetMint.toBase58()
30
+ const edgesByPriceMint = buildEdgesByPriceMint(prices)
31
+ const visited = new Set<string>([startKey])
32
+ const queue: string[] = [startKey]
33
+ const previous = new Map<string, { parent: string; edge: PriceEdge }>()
34
+ const mintByKey = new Map<string, PublicKey>([
35
+ [startKey, sourceMint],
36
+ [targetKey, targetMint],
37
+ ])
38
+
39
+ while (queue.length > 0) {
40
+ const currentKey = queue.shift()
41
+ if (!currentKey) {
42
+ continue
43
+ }
44
+
45
+ for (const edge of edgesByPriceMint.get(currentKey) ?? []) {
46
+ const nextKey = edge.underlyingMint.toBase58()
47
+
48
+ previous.set(nextKey, { parent: currentKey, edge })
49
+ mintByKey.set(edge.priceMint.toBase58(), edge.priceMint)
50
+ mintByKey.set(nextKey, edge.underlyingMint)
51
+
52
+ if (nextKey === targetKey) {
53
+ return buildResolvedPath(previous, mintByKey, startKey, targetKey)
54
+ }
55
+
56
+ if (visited.has(nextKey)) {
57
+ continue
58
+ }
59
+
60
+ visited.add(nextKey)
61
+ queue.push(nextKey)
62
+ }
63
+ }
64
+
65
+ return null
66
+ }
67
+
68
+ export function resolvePriceIdFromMintToUnderlying(
69
+ prices: ExponentPrices,
70
+ sourceMint: PublicKey,
71
+ targetMint: PublicKey,
72
+ ): PriceId | null {
73
+ const path = resolveExponentPricePath(prices, sourceMint, targetMint)
74
+ if (!path) {
75
+ return null
76
+ }
77
+ return priceIdFromPath(path)
78
+ }
79
+
80
+ export function resolvePriceIdFromMintToUnderlyingOrThrow(params: {
81
+ prices: ExponentPrices
82
+ sourceMint: PublicKey
83
+ targetMint: PublicKey
84
+ label: string
85
+ }): PriceId {
86
+ const resolved = resolvePriceIdFromMintToUnderlying(params.prices, params.sourceMint, params.targetMint)
87
+ if (resolved) {
88
+ return resolved
89
+ }
90
+
91
+ throw new Error(
92
+ `Missing Exponent price path for ${params.label}: ${params.sourceMint.toBase58()} -> ${params.targetMint.toBase58()}`,
93
+ )
94
+ }
95
+
96
+ /**
97
+ * Resolve the Kamino quote path for a vault.
98
+ *
99
+ * The quote mint is always the vault's underlying mint. Each reserve's
100
+ * price chains to this mint, and the `quotePriceId` is the identity
101
+ * price (the `One` entry for the vault underlying).
102
+ */
103
+ export function resolveBestKaminoQuotePath(params: {
104
+ prices: ExponentPrices
105
+ vaultUnderlyingMint: PublicKey
106
+ }): ResolvedKaminoQuotePath {
107
+ const quotePriceId = resolvePriceIdFromMintToUnderlying(
108
+ params.prices,
109
+ params.vaultUnderlyingMint,
110
+ params.vaultUnderlyingMint,
111
+ )
112
+ if (!quotePriceId) {
113
+ throw new Error(
114
+ `Missing Exponent price path for Kamino quote resolution: no price entry for vault underlying ${params.vaultUnderlyingMint.toBase58()} to itself`,
115
+ )
116
+ }
117
+
118
+ return {
119
+ quoteInputMint: params.vaultUnderlyingMint,
120
+ quotePriceId,
121
+ }
122
+ }
123
+
124
+ export function resolveKaminoReservePriceIdOrThrow(params: {
125
+ prices: ExponentPrices
126
+ reserveMint: PublicKey
127
+ quoteInputMint: PublicKey
128
+ }): PriceId {
129
+ if (params.reserveMint.equals(params.quoteInputMint)) {
130
+ return priceId("Simple", { priceId: 0n })
131
+ }
132
+
133
+ const reservePath = resolveExponentPricePath(params.prices, params.reserveMint, params.quoteInputMint)
134
+ if (!reservePath) {
135
+ throw new Error(
136
+ `Missing Exponent price path for Kamino reserve mapping: ${params.reserveMint.toBase58()} -> ${params.quoteInputMint.toBase58()}`,
137
+ )
138
+ }
139
+
140
+ return priceIdFromPath(reservePath)
141
+ }
142
+
143
+ export function extractPriceIds(priceIdValue: unknown): bigint[] {
144
+ if (!priceIdValue || typeof priceIdValue !== "object") {
145
+ throw new Error("Invalid PriceId: expected an object")
146
+ }
147
+
148
+ const value = priceIdValue as Record<string, unknown>
149
+ if (value.__kind === "Simple" && typeof value.priceId === "bigint") {
150
+ return [value.priceId]
151
+ }
152
+ if (value.__kind === "Multiply" && Array.isArray(value.priceIds)) {
153
+ return value.priceIds.map(asBigInt)
154
+ }
155
+ if ("simple" in value) {
156
+ const simple = value.simple as Record<string, unknown> | undefined
157
+ const raw = simple?.priceId
158
+ if (typeof raw === "bigint" || typeof raw === "number") {
159
+ return [BigInt(raw)]
160
+ }
161
+ }
162
+ if ("multiply" in value) {
163
+ const multiply = value.multiply as Record<string, unknown> | undefined
164
+ const rawIds = multiply?.priceIds
165
+ if (Array.isArray(rawIds)) {
166
+ return rawIds.map(asBigInt)
167
+ }
168
+ }
169
+
170
+ throw new Error("Unsupported PriceId shape")
171
+ }
172
+
173
+ export function getPriceInputMintFromPriceId(prices: ExponentPrices, priceIdValue: unknown): PublicKey {
174
+ const ids = extractPriceIds(priceIdValue)
175
+ const lastId = ids[ids.length - 1]
176
+ const entry = findPriceEntryById(prices, lastId)
177
+ if (!entry) {
178
+ throw new Error(`Price entry not found for id ${lastId.toString()}`)
179
+ }
180
+ return entry.priceMint
181
+ }
182
+
183
+ function buildEdgesByPriceMint(prices: ExponentPrices): Map<string, PriceEdge[]> {
184
+ const result = new Map<string, PriceEdge[]>()
185
+
186
+ for (const entry of prices.prices) {
187
+ if (!entry) {
188
+ continue
189
+ }
190
+
191
+ const key = entry.priceMint.toBase58()
192
+ const edges = result.get(key) ?? []
193
+ edges.push({
194
+ priceId: entry.priceId,
195
+ priceMint: entry.priceMint,
196
+ underlyingMint: entry.underlyingMint,
197
+ })
198
+ result.set(key, edges)
199
+ }
200
+
201
+ return result
202
+ }
203
+
204
+ function buildResolvedPath(
205
+ previous: Map<string, { parent: string; edge: PriceEdge }>,
206
+ mintByKey: Map<string, PublicKey>,
207
+ startKey: string,
208
+ targetKey: string,
209
+ ): ResolvedPricePath {
210
+ const reversedEdges: PriceEdge[] = []
211
+ const reversedMints: PublicKey[] = []
212
+
213
+ let cursor = targetKey
214
+ reversedMints.push(mintByKey.get(cursor) ?? new PublicKey(cursor))
215
+
216
+ if (startKey === targetKey) {
217
+ const step = previous.get(targetKey)
218
+ if (step) {
219
+ reversedEdges.push(step.edge)
220
+ reversedMints.push(mintByKey.get(startKey) ?? new PublicKey(startKey))
221
+ }
222
+ } else {
223
+ while (cursor !== startKey) {
224
+ const step = previous.get(cursor)
225
+ if (!step) {
226
+ throw new Error("Failed to reconstruct price path")
227
+ }
228
+
229
+ reversedEdges.push(step.edge)
230
+ cursor = step.parent
231
+ reversedMints.push(mintByKey.get(cursor) ?? new PublicKey(cursor))
232
+ }
233
+ }
234
+
235
+ const mints = reversedMints.reverse()
236
+ const edges = reversedEdges.reverse()
237
+ return {
238
+ sourceMint: mints[0]!,
239
+ targetMint: mints[mints.length - 1]!,
240
+ edges,
241
+ mints,
242
+ }
243
+ }
244
+
245
+ function priceIdFromPath(path: ResolvedPricePath): PriceId {
246
+ const ids = [...path.edges].reverse().map((edge) => edge.priceId)
247
+ if (ids.length === 0) {
248
+ throw new Error("Price path cannot be empty")
249
+ }
250
+
251
+ return ids.length === 1
252
+ ? priceId("Simple", { priceId: ids[0]! })
253
+ : priceId("Multiply", { priceIds: ids })
254
+ }
255
+
256
+ function findPriceEntryById(prices: ExponentPrices, priceIdValue: bigint): ExponentPrice | null {
257
+ for (const entry of prices.prices) {
258
+ if (entry?.priceId === priceIdValue) {
259
+ return entry
260
+ }
261
+ }
262
+ return null
263
+ }
264
+
265
+ function asBigInt(value: unknown): bigint {
266
+ if (typeof value === "bigint") {
267
+ return value
268
+ }
269
+ if (typeof value === "number") {
270
+ return BigInt(value)
271
+ }
272
+ throw new Error("Expected bigint-compatible price id")
273
+ }
@@ -115,7 +115,12 @@ export function compileToSyncTransactionPayload({
115
115
  }
116
116
 
117
117
  // Add instruction accounts
118
- for (const key of ix.keys) {
118
+ for (const [keyIndex, key] of ix.keys.entries()) {
119
+ if (!key?.pubkey) {
120
+ throw new Error(
121
+ `Instruction ${ix.programId.toBase58()} is missing account meta at index ${keyIndex}`,
122
+ )
123
+ }
119
124
  const keyStr = key.pubkey.toBase58()
120
125
  const existing = accountMap.get(keyStr)
121
126
  if (existing) {
@@ -1,17 +1,28 @@
1
1
  import {
2
2
  Connection,
3
3
  PublicKey,
4
+ Keypair,
4
5
  TransactionInstruction,
5
6
  VersionedTransaction,
6
7
  TransactionMessage,
7
8
  AddressLookupTableAccount,
8
9
  } from "@solana/web3.js"
10
+ import { getAssociatedTokenAddressSync, TOKEN_PROGRAM_ID } from "@solana/spl-token"
9
11
  import { encode, decode } from "@msgpack/msgpack"
10
12
  import WebSocket from "ws"
11
13
 
12
14
  const TITAN_WS_SUBPROTOCOL = "v1.api.titan.ag"
13
15
  const SWAP_ROUTE_V2_DISCRIMINATOR = Buffer.from([249, 91, 84, 33, 69, 22, 0, 135])
14
16
  const QUOTE_TIMEOUT_MS = 45_000
17
+ const INCOMPATIBLE_ROUTE_RETRY_ATTEMPTS = 3
18
+ const INCOMPATIBLE_ROUTE_RETRY_DELAY_MS = 750
19
+ const TITAN_QUOTE_STREAM_ROUTE_COUNT = 6
20
+ const TITAN_INPUT_MINT_ACCOUNT_INDEX = 2
21
+ const TITAN_INPUT_TOKEN_ACCOUNT_INDEX = 3
22
+ const TITAN_OUTPUT_MINT_ACCOUNT_INDEX = 4
23
+ const TITAN_OUTPUT_TOKEN_ACCOUNT_INDEX = 5
24
+ const TITAN_INPUT_TOKEN_PROGRAM_ACCOUNT_INDEX = 6
25
+ const TITAN_OUTPUT_TOKEN_PROGRAM_ACCOUNT_INDEX = 7
15
26
 
16
27
  // --- Titan wire-format types (MessagePack-decoded) ---
17
28
 
@@ -60,6 +71,10 @@ export interface TitanQuoteResult {
60
71
  addressLookupTableAddresses: PublicKey[]
61
72
  }
62
73
 
74
+ const TITAN_VAULT_INCOMPATIBLE_PROGRAM_IDS = new Set<string>([
75
+ "ALPHAQmeA7bjrVuccPsYPiCvsi428SNwte66Srvs4pHA",
76
+ ])
77
+
63
78
  const JITO_BUNDLE_BLOCKED_ACCOUNT_PREFIXES = ["jitodontfront", "jitonobundle"]
64
79
 
65
80
  /**
@@ -71,36 +86,51 @@ export async function getTitanQuote(
71
86
  auth: { wsUrl: string; authToken: string },
72
87
  params: TitanSwapParams,
73
88
  ): Promise<TitanQuoteResult> {
74
- const route = await fetchBestRoute(auth, params)
75
- const addressLookupTableAddresses = (route.addressLookupTables ?? []).map(
76
- (bytes) => new PublicKey(bytes),
77
- )
89
+ const candidates = buildQuoteCandidates(params.userPublicKey)
90
+ let lastBlockedRoute: TitanQuoteResult | null = null
91
+ let lastError: Error | null = null
92
+
93
+ for (let attempt = 1; attempt <= INCOMPATIBLE_ROUTE_RETRY_ATTEMPTS; attempt++) {
94
+ for (const candidate of candidates) {
95
+ try {
96
+ const routes = await fetchUsableRoutes(auth, {
97
+ ...params,
98
+ userPublicKey: candidate.quoteUserPublicKey,
99
+ })
100
+
101
+ for (const route of routes) {
102
+ const quote = await buildQuoteResult(connection, route)
103
+ const normalizedQuote = candidate.rewriteToUserPublicKey
104
+ ? rewriteTitanQuoteForOwner({
105
+ quote,
106
+ quotedUserPublicKey: candidate.quoteUserPublicKey,
107
+ actualUserPublicKey: candidate.rewriteToUserPublicKey,
108
+ })
109
+ : quote
110
+
111
+ if (isVaultCompatibleQuote(normalizedQuote.instruction, params.userPublicKey)) {
112
+ return normalizedQuote
113
+ }
114
+ lastBlockedRoute = normalizedQuote
115
+ }
116
+ } catch (error) {
117
+ lastError = asError(error)
118
+ }
119
+ }
78
120
 
79
- // Path 1: Full transaction bytes deserialize and extract the swap ix
80
- if (route.transaction) {
81
- const vTx = VersionedTransaction.deserialize(new Uint8Array(route.transaction))
82
- const instruction = await extractSwapInstruction(connection, vTx)
83
- return { instruction, addressLookupTableAddresses }
121
+ if (attempt < INCOMPATIBLE_ROUTE_RETRY_ATTEMPTS && (lastBlockedRoute || lastError)) {
122
+ await sleep(INCOMPATIBLE_ROUTE_RETRY_DELAY_MS)
123
+ }
84
124
  }
85
125
 
86
- // Path 2: Reconstruct from the route's instructions array
87
- if (route.instructions && route.instructions.length > 0) {
88
- const swapIx = route.instructions.find((ix) => {
89
- return ix.d.length >= 8 && Buffer.from(ix.d.subarray(0, 8)).equals(SWAP_ROUTE_V2_DISCRIMINATOR)
90
- })
126
+ if (lastBlockedRoute) {
127
+ throw new Error(
128
+ `Titan returned only routes incompatible with vault-owned execution after ${INCOMPATIBLE_ROUTE_RETRY_ATTEMPTS} attempts`,
129
+ )
130
+ }
91
131
 
92
- if (swapIx) {
93
- const instruction = new TransactionInstruction({
94
- programId: new PublicKey(swapIx.p),
95
- keys: swapIx.a.map((acc) => ({
96
- pubkey: new PublicKey(acc.p),
97
- isSigner: acc.s,
98
- isWritable: acc.w,
99
- })),
100
- data: Buffer.from(swapIx.d),
101
- })
102
- return { instruction: sanitizeTitanInstructionForBundles(instruction), addressLookupTableAddresses }
103
- }
132
+ if (lastError) {
133
+ throw lastError
104
134
  }
105
135
 
106
136
  throw new Error("Titan route has neither transaction bytes nor a SwapRouteV2 instruction")
@@ -108,27 +138,63 @@ export async function getTitanQuote(
108
138
 
109
139
  // --- Internals ---
110
140
 
111
- function fetchBestRoute(
141
+ interface QuoteCandidate {
142
+ quoteUserPublicKey: PublicKey
143
+ rewriteToUserPublicKey?: PublicKey
144
+ }
145
+
146
+ function buildQuoteCandidates(userPublicKey: PublicKey): QuoteCandidate[] {
147
+ if (!PublicKey.isOnCurve(userPublicKey.toBytes())) {
148
+ return [
149
+ { quoteUserPublicKey: userPublicKey },
150
+ {
151
+ quoteUserPublicKey: deriveTitanQuoteSurrogateUser(userPublicKey),
152
+ rewriteToUserPublicKey: userPublicKey,
153
+ },
154
+ ]
155
+ }
156
+ return [{ quoteUserPublicKey: userPublicKey }]
157
+ }
158
+
159
+ function deriveTitanQuoteSurrogateUser(userPublicKey: PublicKey): PublicKey {
160
+ return Keypair.fromSeed(userPublicKey.toBytes()).publicKey
161
+ }
162
+
163
+ function fetchUsableRoutes(
112
164
  auth: { wsUrl: string; authToken: string },
113
165
  params: TitanSwapParams,
114
- ): Promise<TitanRoute> {
166
+ ): Promise<TitanRoute[]> {
115
167
  return new Promise((resolve, reject) => {
116
168
  const url = `${auth.wsUrl}?auth=${auth.authToken}`
117
169
  const ws = new WebSocket(url, [TITAN_WS_SUBPROTOCOL])
118
170
  ws.binaryType = "arraybuffer"
171
+ const collectedRoutes: TitanRoute[] = []
172
+ let settled = false
119
173
 
120
174
  const timeout = setTimeout(() => {
121
- ws.close()
175
+ cleanup()
176
+ if (collectedRoutes.length > 0) {
177
+ resolve(selectUsableRoutes(collectedRoutes))
178
+ return
179
+ }
122
180
  reject(new Error("Titan quote timed out after " + QUOTE_TIMEOUT_MS + "ms"))
123
181
  }, QUOTE_TIMEOUT_MS)
124
182
 
125
183
  const cleanup = () => {
184
+ settled = true
126
185
  clearTimeout(timeout)
127
186
  ws.close()
128
187
  }
129
188
 
130
189
  ws.on("error", (err) => {
190
+ if (settled) {
191
+ return
192
+ }
131
193
  cleanup()
194
+ if (collectedRoutes.length > 0) {
195
+ resolve(selectUsableRoutes(collectedRoutes))
196
+ return
197
+ }
132
198
  reject(new Error("Titan WebSocket error: " + (err as Error).message))
133
199
  })
134
200
 
@@ -143,22 +209,29 @@ function fetchBestRoute(
143
209
  amount: params.amount,
144
210
  slippageBps: params.slippageBps ?? 50,
145
211
  },
146
- transaction: {
147
- userPublicKey: params.userPublicKey.toBytes(),
148
- },
149
- update: {
150
- num_quotes: 3,
151
- },
152
- },
153
- },
212
+ transaction: {
213
+ userPublicKey: params.userPublicKey.toBytes(),
214
+ },
215
+ update: {
216
+ num_quotes: TITAN_QUOTE_STREAM_ROUTE_COUNT,
217
+ },
218
+ },
219
+ },
154
220
  }
155
221
  ws.send(encode(request, { useBigInt64: true }))
156
222
  })
157
223
 
158
224
  ws.on("message", (data: ArrayBuffer) => {
225
+ if (settled) {
226
+ return
227
+ }
159
228
  const msg = decode(new Uint8Array(data), { useBigInt64: true }) as TitanServerMessage
160
229
  if (msg.Error) {
161
230
  cleanup()
231
+ if (collectedRoutes.length > 0) {
232
+ resolve(selectUsableRoutes(collectedRoutes))
233
+ return
234
+ }
162
235
  reject(new Error(`Titan API error [${msg.Error.code}]: ${msg.Error.message}`))
163
236
  return
164
237
  }
@@ -167,16 +240,15 @@ function fetchBestRoute(
167
240
  const quotes = msg.StreamData.payload?.SwapQuotes
168
241
  if (!quotes) return
169
242
 
170
- const best = selectBestUsableRoute(Object.values(quotes.quotes))
171
- if (best) {
172
- cleanup()
173
- resolve(best)
174
- return
175
- }
243
+ collectedRoutes.push(...Object.values(quotes.quotes).filter(isUsableTitanRoute))
176
244
  }
177
245
 
178
246
  if (msg.StreamEnd) {
179
247
  cleanup()
248
+ if (collectedRoutes.length > 0) {
249
+ resolve(selectUsableRoutes(collectedRoutes))
250
+ return
251
+ }
180
252
  const err = msg.StreamEnd.errorMessage
181
253
  ? `Titan stream ended with error [${msg.StreamEnd.errorCode}]: ${msg.StreamEnd.errorMessage}`
182
254
  : "Titan stream ended unexpectedly"
@@ -186,10 +258,10 @@ function fetchBestRoute(
186
258
  })
187
259
  }
188
260
 
189
- function selectBestUsableRoute(routes: TitanRoute[]) {
261
+ function selectUsableRoutes(routes: TitanRoute[]) {
190
262
  return [...routes]
191
263
  .sort((a, b) => (a.outAmount > b.outAmount ? -1 : a.outAmount < b.outAmount ? 1 : 0))
192
- .find(isUsableTitanRoute)
264
+ .filter(isUsableTitanRoute)
193
265
  }
194
266
 
195
267
  function isUsableTitanRoute(route: TitanRoute) {
@@ -204,6 +276,120 @@ function isUsableTitanRoute(route: TitanRoute) {
204
276
  )
205
277
  }
206
278
 
279
+ function sleep(ms: number) {
280
+ return new Promise((resolve) => setTimeout(resolve, ms))
281
+ }
282
+
283
+ function asError(error: unknown): Error {
284
+ return error instanceof Error ? error : new Error(String(error))
285
+ }
286
+
287
+ async function buildQuoteResult(
288
+ connection: Connection,
289
+ route: TitanRoute,
290
+ ): Promise<TitanQuoteResult> {
291
+ const addressLookupTableAddresses = (route.addressLookupTables ?? []).map(
292
+ (bytes) => new PublicKey(bytes),
293
+ )
294
+
295
+ if (route.transaction) {
296
+ const vTx = VersionedTransaction.deserialize(new Uint8Array(route.transaction))
297
+ const instruction = await extractSwapInstruction(connection, vTx)
298
+ return { instruction, addressLookupTableAddresses }
299
+ }
300
+
301
+ if (route.instructions && route.instructions.length > 0) {
302
+ const swapIx = route.instructions.find((ix) => {
303
+ return ix.d.length >= 8 && Buffer.from(ix.d.subarray(0, 8)).equals(SWAP_ROUTE_V2_DISCRIMINATOR)
304
+ })
305
+
306
+ if (swapIx) {
307
+ const instruction = new TransactionInstruction({
308
+ programId: new PublicKey(swapIx.p),
309
+ keys: swapIx.a.map((acc) => ({
310
+ pubkey: new PublicKey(acc.p),
311
+ isSigner: acc.s,
312
+ isWritable: acc.w,
313
+ })),
314
+ data: Buffer.from(swapIx.d),
315
+ })
316
+ return { instruction: sanitizeTitanInstructionForBundles(instruction), addressLookupTableAddresses }
317
+ }
318
+ }
319
+
320
+ throw new Error("Titan route has neither transaction bytes nor a SwapRouteV2 instruction")
321
+ }
322
+
323
+ function rewriteTitanQuoteForOwner(params: {
324
+ quote: TitanQuoteResult
325
+ quotedUserPublicKey: PublicKey
326
+ actualUserPublicKey: PublicKey
327
+ }): TitanQuoteResult {
328
+ return {
329
+ instruction: rewriteTitanInstructionForOwner(
330
+ params.quote.instruction,
331
+ params.quotedUserPublicKey,
332
+ params.actualUserPublicKey,
333
+ ),
334
+ addressLookupTableAddresses: params.quote.addressLookupTableAddresses,
335
+ }
336
+ }
337
+
338
+ function rewriteTitanInstructionForOwner(
339
+ instruction: TransactionInstruction,
340
+ quotedUserPublicKey: PublicKey,
341
+ actualUserPublicKey: PublicKey,
342
+ ): TransactionInstruction {
343
+ const rewrittenKeys = instruction.keys.map((key) => ({
344
+ pubkey: key.pubkey,
345
+ isSigner: key.isSigner,
346
+ isWritable: key.isWritable,
347
+ }))
348
+
349
+ const inputMint = instruction.keys[TITAN_INPUT_MINT_ACCOUNT_INDEX]?.pubkey
350
+ const outputMint = instruction.keys[TITAN_OUTPUT_MINT_ACCOUNT_INDEX]?.pubkey
351
+ const inputTokenProgram = instruction.keys[TITAN_INPUT_TOKEN_PROGRAM_ACCOUNT_INDEX]?.pubkey ?? TOKEN_PROGRAM_ID
352
+ const outputTokenProgram = instruction.keys[TITAN_OUTPUT_TOKEN_PROGRAM_ACCOUNT_INDEX]?.pubkey ?? TOKEN_PROGRAM_ID
353
+
354
+ const replacePubkey = (from: PublicKey, to: PublicKey) => {
355
+ for (const key of rewrittenKeys) {
356
+ if (key.pubkey.equals(from)) {
357
+ key.pubkey = to
358
+ }
359
+ }
360
+ }
361
+
362
+ replacePubkey(quotedUserPublicKey, actualUserPublicKey)
363
+
364
+ if (inputMint) {
365
+ replacePubkey(
366
+ getAssociatedTokenAddressSync(inputMint, quotedUserPublicKey, true, inputTokenProgram),
367
+ getAssociatedTokenAddressSync(inputMint, actualUserPublicKey, true, inputTokenProgram),
368
+ )
369
+ }
370
+
371
+ if (outputMint) {
372
+ replacePubkey(
373
+ getAssociatedTokenAddressSync(outputMint, quotedUserPublicKey, true, outputTokenProgram),
374
+ getAssociatedTokenAddressSync(outputMint, actualUserPublicKey, true, outputTokenProgram),
375
+ )
376
+ }
377
+
378
+ return new TransactionInstruction({
379
+ programId: instruction.programId,
380
+ keys: rewrittenKeys,
381
+ data: instruction.data,
382
+ })
383
+ }
384
+
385
+ function isVaultCompatibleQuote(instruction: TransactionInstruction, userPublicKey: PublicKey) {
386
+ if (instruction.keys.some((key) => TITAN_VAULT_INCOMPATIBLE_PROGRAM_IDS.has(key.pubkey.toBase58()))) {
387
+ return false
388
+ }
389
+
390
+ return !instruction.keys.some((key) => key.isSigner && !key.pubkey.equals(userPublicKey))
391
+ }
392
+
207
393
  async function extractSwapInstruction(
208
394
  connection: Connection,
209
395
  vTx: VersionedTransaction,