@exponent-labs/exponent-sdk 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/client/vaults/index.d.ts +2 -0
- package/build/client/vaults/index.js +2 -0
- package/build/client/vaults/index.js.map +1 -1
- package/build/client/vaults/types/index.d.ts +2 -0
- package/build/client/vaults/types/index.js +2 -0
- package/build/client/vaults/types/index.js.map +1 -1
- package/build/client/vaults/types/kaminoFarmEntry.d.ts +15 -0
- package/build/client/vaults/types/kaminoFarmEntry.js +17 -0
- package/build/client/vaults/types/kaminoFarmEntry.js.map +1 -0
- package/build/client/vaults/types/kaminoObligationEntry.d.ts +21 -4
- package/build/client/vaults/types/kaminoObligationEntry.js +2 -1
- package/build/client/vaults/types/kaminoObligationEntry.js.map +1 -1
- package/build/client/vaults/types/positionUpdate.d.ts +9 -0
- package/build/client/vaults/types/positionUpdate.js +23 -0
- package/build/client/vaults/types/positionUpdate.js.map +1 -1
- package/build/client/vaults/types/proposalAction.js +0 -3
- package/build/client/vaults/types/proposalAction.js.map +1 -1
- package/build/client/vaults/types/reserveFarmMapping.d.ts +19 -0
- package/build/client/vaults/types/reserveFarmMapping.js +18 -0
- package/build/client/vaults/types/reserveFarmMapping.js.map +1 -0
- package/build/client/vaults/types/strategyPosition.d.ts +5 -0
- package/build/client/vaults/types/strategyPosition.js +5 -0
- package/build/client/vaults/types/strategyPosition.js.map +1 -1
- package/build/exponentVaults/aumCalculator.d.ts +25 -4
- package/build/exponentVaults/aumCalculator.js +236 -15
- package/build/exponentVaults/aumCalculator.js.map +1 -1
- package/build/exponentVaults/fetcher.d.ts +52 -0
- package/build/exponentVaults/fetcher.js +199 -0
- package/build/exponentVaults/fetcher.js.map +1 -0
- package/build/exponentVaults/index.d.ts +10 -9
- package/build/exponentVaults/index.js +26 -8
- package/build/exponentVaults/index.js.map +1 -1
- package/build/exponentVaults/kamino-farms.d.ts +144 -0
- package/build/exponentVaults/kamino-farms.js +396 -0
- package/build/exponentVaults/kamino-farms.js.map +1 -0
- package/build/exponentVaults/loopscale/client.d.ts +240 -0
- package/build/exponentVaults/loopscale/client.js +590 -0
- package/build/exponentVaults/loopscale/client.js.map +1 -0
- package/build/exponentVaults/loopscale/client.test.d.ts +1 -0
- package/build/exponentVaults/loopscale/client.test.js +183 -0
- package/build/exponentVaults/loopscale/client.test.js.map +1 -0
- package/build/exponentVaults/loopscale/helpers.d.ts +29 -0
- package/build/exponentVaults/loopscale/helpers.js +119 -0
- package/build/exponentVaults/loopscale/helpers.js.map +1 -0
- package/build/exponentVaults/loopscale/index.d.ts +3 -0
- package/build/exponentVaults/loopscale/index.js +12 -0
- package/build/exponentVaults/loopscale/index.js.map +1 -0
- package/build/exponentVaults/loopscale/prepared-transactions.d.ts +13 -0
- package/build/exponentVaults/loopscale/prepared-transactions.js +271 -0
- package/build/exponentVaults/loopscale/prepared-transactions.js.map +1 -0
- package/build/exponentVaults/loopscale/prepared-transactions.test.d.ts +1 -0
- package/build/exponentVaults/loopscale/prepared-transactions.test.js +400 -0
- package/build/exponentVaults/loopscale/prepared-transactions.test.js.map +1 -0
- package/build/exponentVaults/loopscale/prepared-types.d.ts +62 -0
- package/build/exponentVaults/loopscale/prepared-types.js +3 -0
- package/build/exponentVaults/loopscale/prepared-types.js.map +1 -0
- package/build/exponentVaults/loopscale/response-plan.d.ts +69 -0
- package/build/exponentVaults/loopscale/response-plan.js +141 -0
- package/build/exponentVaults/loopscale/response-plan.js.map +1 -0
- package/build/exponentVaults/loopscale/response-plan.test.d.ts +1 -0
- package/build/exponentVaults/loopscale/response-plan.test.js +139 -0
- package/build/exponentVaults/loopscale/response-plan.test.js.map +1 -0
- package/build/exponentVaults/loopscale/send-plan.d.ts +75 -0
- package/build/exponentVaults/loopscale/send-plan.js +235 -0
- package/build/exponentVaults/loopscale/send-plan.js.map +1 -0
- package/build/exponentVaults/loopscale/types.d.ts +443 -0
- package/build/exponentVaults/loopscale/types.js +3 -0
- package/build/exponentVaults/loopscale/types.js.map +1 -0
- package/build/exponentVaults/loopscale-client.d.ts +113 -524
- package/build/exponentVaults/loopscale-client.js +296 -539
- package/build/exponentVaults/loopscale-client.js.map +1 -1
- package/build/exponentVaults/loopscale-client.test.d.ts +1 -0
- package/build/exponentVaults/loopscale-client.test.js +162 -0
- package/build/exponentVaults/loopscale-client.test.js.map +1 -0
- package/build/exponentVaults/loopscale-client.types.d.ts +425 -0
- package/build/exponentVaults/loopscale-client.types.js +3 -0
- package/build/exponentVaults/loopscale-client.types.js.map +1 -0
- package/build/exponentVaults/loopscale-execution.d.ts +125 -0
- package/build/exponentVaults/loopscale-execution.js +341 -0
- package/build/exponentVaults/loopscale-execution.js.map +1 -0
- package/build/exponentVaults/loopscale-execution.test.d.ts +1 -0
- package/build/exponentVaults/loopscale-execution.test.js +139 -0
- package/build/exponentVaults/loopscale-execution.test.js.map +1 -0
- package/build/exponentVaults/loopscale-vault.d.ts +115 -0
- package/build/exponentVaults/loopscale-vault.js +275 -0
- package/build/exponentVaults/loopscale-vault.js.map +1 -0
- package/build/exponentVaults/loopscale-vault.test.d.ts +1 -0
- package/build/exponentVaults/loopscale-vault.test.js +102 -0
- package/build/exponentVaults/loopscale-vault.test.js.map +1 -0
- package/build/exponentVaults/policyBuilders.d.ts +62 -0
- package/build/exponentVaults/policyBuilders.js +119 -2
- package/build/exponentVaults/policyBuilders.js.map +1 -1
- package/build/exponentVaults/pricePathResolver.d.ts +45 -0
- package/build/exponentVaults/pricePathResolver.js +198 -0
- package/build/exponentVaults/pricePathResolver.js.map +1 -0
- package/build/exponentVaults/pricePathResolver.test.d.ts +1 -0
- package/build/exponentVaults/pricePathResolver.test.js +369 -0
- package/build/exponentVaults/pricePathResolver.test.js.map +1 -0
- package/build/exponentVaults/syncTransaction.js +4 -1
- package/build/exponentVaults/syncTransaction.js.map +1 -1
- package/build/exponentVaults/titan-quote.js +170 -36
- package/build/exponentVaults/titan-quote.js.map +1 -1
- package/build/exponentVaults/vault-instruction-types.d.ts +363 -0
- package/build/exponentVaults/vault-instruction-types.js +128 -0
- package/build/exponentVaults/vault-instruction-types.js.map +1 -0
- package/build/exponentVaults/vault-interaction.d.ts +203 -343
- package/build/exponentVaults/vault-interaction.js +1894 -426
- package/build/exponentVaults/vault-interaction.js.map +1 -1
- package/build/exponentVaults/vault-interaction.kamino-vault.test.d.ts +1 -0
- package/build/exponentVaults/vault-interaction.kamino-vault.test.js +143 -0
- package/build/exponentVaults/vault-interaction.kamino-vault.test.js.map +1 -0
- package/build/exponentVaults/vault.d.ts +51 -2
- package/build/exponentVaults/vault.js +324 -48
- package/build/exponentVaults/vault.js.map +1 -1
- package/build/exponentVaults/vaultTransactionBuilder.d.ts +100 -134
- package/build/exponentVaults/vaultTransactionBuilder.js +383 -285
- package/build/exponentVaults/vaultTransactionBuilder.js.map +1 -1
- package/build/exponentVaults/vaultTransactionBuilder.test.d.ts +1 -0
- package/build/exponentVaults/vaultTransactionBuilder.test.js +297 -0
- package/build/exponentVaults/vaultTransactionBuilder.test.js.map +1 -0
- package/build/marketThree.d.ts +6 -2
- package/build/marketThree.js +10 -8
- package/build/marketThree.js.map +1 -1
- package/package.json +34 -32
- package/src/client/vaults/index.ts +2 -0
- package/src/client/vaults/types/index.ts +2 -0
- package/src/client/vaults/types/kaminoFarmEntry.ts +32 -0
- package/src/client/vaults/types/kaminoObligationEntry.ts +6 -3
- package/src/client/vaults/types/positionUpdate.ts +62 -0
- package/src/client/vaults/types/proposalAction.ts +0 -3
- package/src/client/vaults/types/reserveFarmMapping.ts +35 -0
- package/src/client/vaults/types/strategyPosition.ts +18 -1
- package/src/exponentVaults/aumCalculator.ts +353 -16
- package/src/exponentVaults/fetcher.ts +257 -0
- package/src/exponentVaults/index.ts +65 -40
- package/src/exponentVaults/kamino-farms.ts +538 -0
- package/src/exponentVaults/loopscale/client.ts +808 -0
- package/src/exponentVaults/loopscale/helpers.ts +172 -0
- package/src/exponentVaults/loopscale/index.ts +57 -0
- package/src/exponentVaults/loopscale/prepared-transactions.ts +435 -0
- package/src/exponentVaults/loopscale/prepared-types.ts +73 -0
- package/src/exponentVaults/loopscale/types.ts +466 -0
- package/src/exponentVaults/policyBuilders.ts +170 -0
- package/src/exponentVaults/pricePathResolver.test.ts +466 -0
- package/src/exponentVaults/pricePathResolver.ts +273 -0
- package/src/exponentVaults/syncTransaction.ts +6 -1
- package/src/exponentVaults/titan-quote.ts +231 -45
- package/src/exponentVaults/vault-instruction-types.ts +493 -0
- package/src/exponentVaults/vault-interaction.kamino-vault.test.ts +149 -0
- package/src/exponentVaults/vault-interaction.ts +2818 -799
- package/src/exponentVaults/vault.ts +474 -63
- package/src/exponentVaults/vaultTransactionBuilder.test.ts +349 -0
- package/src/exponentVaults/vaultTransactionBuilder.ts +581 -433
- package/src/marketThree.ts +14 -6
- package/src/exponentVaults/loopscale-client.ts +0 -1373
|
@@ -0,0 +1,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
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
.
|
|
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,
|