@velocity-exchange/sdk 0.9.0 → 0.10.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 (66) hide show
  1. package/lib/browser/adminClient.d.ts +2 -2
  2. package/lib/browser/adminClient.js +3 -11
  3. package/lib/browser/equityFloorManager.d.ts +9 -5
  4. package/lib/browser/equityFloorManager.js +9 -5
  5. package/lib/browser/idl/velocity.d.ts +2 -1
  6. package/lib/browser/idl/velocity.json +2 -1
  7. package/lib/browser/index.d.ts +2 -0
  8. package/lib/browser/index.js +3 -0
  9. package/lib/browser/jupiter/jupiterClient.d.ts +31 -27
  10. package/lib/browser/jupiter/jupiterClient.js +61 -35
  11. package/lib/browser/math/margin.d.ts +12 -10
  12. package/lib/browser/math/margin.js +16 -14
  13. package/lib/browser/math/superStake.d.ts +9 -9
  14. package/lib/browser/math/superStake.js +8 -8
  15. package/lib/browser/swap/UnifiedSwapClient.d.ts +38 -76
  16. package/lib/browser/swap/UnifiedSwapClient.js +46 -117
  17. package/lib/browser/swap/routeInstructions.d.ts +19 -0
  18. package/lib/browser/swap/routeInstructions.js +43 -0
  19. package/lib/browser/swap/types.d.ts +196 -0
  20. package/lib/browser/swap/types.js +88 -0
  21. package/lib/browser/titan/titanClient.d.ts +32 -68
  22. package/lib/browser/titan/titanClient.js +128 -102
  23. package/lib/browser/user.d.ts +16 -18
  24. package/lib/browser/user.js +24 -26
  25. package/lib/browser/velocityClient.d.ts +83 -101
  26. package/lib/browser/velocityClient.js +152 -289
  27. package/lib/node/adminClient.d.ts +2 -2
  28. package/lib/node/adminClient.d.ts.map +1 -1
  29. package/lib/node/adminClient.js +3 -11
  30. package/lib/node/equityFloorManager.d.ts +9 -5
  31. package/lib/node/equityFloorManager.d.ts.map +1 -1
  32. package/lib/node/equityFloorManager.js +9 -5
  33. package/lib/node/idl/velocity.d.ts +2 -1
  34. package/lib/node/idl/velocity.d.ts.map +1 -1
  35. package/lib/node/idl/velocity.json +2 -1
  36. package/lib/node/index.d.ts +2 -0
  37. package/lib/node/index.d.ts.map +1 -1
  38. package/lib/node/index.js +3 -0
  39. package/lib/node/jupiter/jupiterClient.d.ts +31 -27
  40. package/lib/node/jupiter/jupiterClient.d.ts.map +1 -1
  41. package/lib/node/jupiter/jupiterClient.js +61 -35
  42. package/lib/node/math/margin.d.ts +12 -10
  43. package/lib/node/math/margin.d.ts.map +1 -1
  44. package/lib/node/math/margin.js +16 -14
  45. package/lib/node/math/superStake.d.ts +9 -9
  46. package/lib/node/math/superStake.d.ts.map +1 -1
  47. package/lib/node/math/superStake.js +8 -8
  48. package/lib/node/swap/UnifiedSwapClient.d.ts +38 -76
  49. package/lib/node/swap/UnifiedSwapClient.d.ts.map +1 -1
  50. package/lib/node/swap/UnifiedSwapClient.js +46 -117
  51. package/lib/node/swap/routeInstructions.d.ts +20 -0
  52. package/lib/node/swap/routeInstructions.d.ts.map +1 -0
  53. package/lib/node/swap/routeInstructions.js +43 -0
  54. package/lib/node/swap/types.d.ts +197 -0
  55. package/lib/node/swap/types.d.ts.map +1 -0
  56. package/lib/node/swap/types.js +88 -0
  57. package/lib/node/titan/titanClient.d.ts +32 -68
  58. package/lib/node/titan/titanClient.d.ts.map +1 -1
  59. package/lib/node/titan/titanClient.js +128 -102
  60. package/lib/node/user.d.ts +16 -18
  61. package/lib/node/user.d.ts.map +1 -1
  62. package/lib/node/user.js +24 -26
  63. package/lib/node/velocityClient.d.ts +83 -101
  64. package/lib/node/velocityClient.d.ts.map +1 -1
  65. package/lib/node/velocityClient.js +152 -289
  66. package/package.json +1 -1
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.expectProviderRoute = exports.buildSwapQuote = exports.DEFAULT_SWAP_MAX_ACCOUNTS = void 0;
4
+ /** Account budget assumed when a caller doesn't specify one. */
5
+ exports.DEFAULT_SWAP_MAX_ACCOUNTS = 50;
6
+ /**
7
+ * Assembles the {@link SwapQuote} a provider's `getQuote` returns, from the
8
+ * normalized quote and the route payload that will execute it.
9
+ *
10
+ * Providers call this rather than building the object themselves. It records
11
+ * what the route was quoted with next to the payload, which is what lets
12
+ * {@link expectProviderRoute} reject a quote edited after it was returned — so
13
+ * that check needs no cooperation from the provider beyond calling this.
14
+ */
15
+ function buildSwapQuote(quote, providerRoute) {
16
+ const { inputMint, outputMint, inAmount, outAmount, swapMode, slippageBps } = quote;
17
+ const routed = {
18
+ inputMint,
19
+ outputMint,
20
+ inAmount,
21
+ outAmount,
22
+ swapMode,
23
+ slippageBps,
24
+ };
25
+ return {
26
+ ...quote,
27
+ providerRoute: { ...providerRoute, routed },
28
+ };
29
+ }
30
+ exports.buildSwapQuote = buildSwapQuote;
31
+ /** Fields that must describe the same swap on the quote and on its route. */
32
+ const ROUTED_FIELDS = [
33
+ 'inputMint',
34
+ 'outputMint',
35
+ 'inAmount',
36
+ 'outAmount',
37
+ 'swapMode',
38
+ 'slippageBps',
39
+ ];
40
+ /**
41
+ * Throws unless the quote's normalized fields still describe the route it
42
+ * carries.
43
+ *
44
+ * Callers and velocity's swap guards read the normalized fields; the provider
45
+ * executes the opaque payload. Nothing but this ties the two together, so a
46
+ * quote that was copied and edited — `{ ...quote, inAmount }`, a mint rewritten
47
+ * in place — would pass every pair, size and slippage check and then execute the
48
+ * route it was originally quoted for.
49
+ */
50
+ function assertQuoteMatchesRoute(quote, provider, routed) {
51
+ if (!routed) {
52
+ throw new Error(`Quote is missing the route fields recorded at quote time. It must come from ${provider}'s getQuote, which builds it with buildSwapQuote.`);
53
+ }
54
+ for (const field of ROUTED_FIELDS) {
55
+ // Stringified: a provider may normalize a numeric field it decoded.
56
+ if (String(quote[field]) !== String(routed[field])) {
57
+ throw new Error(`Quote reports ${field} ${String(quote[field])} but the route it carries was quoted with ${String(routed[field])}. The quote was modified after it was returned; re-quote instead.`);
58
+ }
59
+ }
60
+ }
61
+ /**
62
+ * Narrows a quote's payload to `provider`, checks it can be executed by
63
+ * `userPublicKey`, and checks it still matches the quote it travels on.
64
+ *
65
+ * A route is wallet-bound when the provider resolves the user's token accounts
66
+ * at quote time (Titan does; Jupiter builds per-wallet at swap time). Those
67
+ * providers record the wallet on `quotedFor`, and executing such a route as
68
+ * anyone else moves funds through accounts the signer doesn't own.
69
+ *
70
+ * @throws If the quote was produced by a different provider, for a wallet other
71
+ * than `userPublicKey`, or for a different pair, size, mode or slippage than the
72
+ * quote now claims.
73
+ */
74
+ function expectProviderRoute(quote, provider, userPublicKey) {
75
+ const route = quote === null || quote === void 0 ? void 0 : quote.providerRoute;
76
+ if (!route) {
77
+ throw new Error(`Quote is missing its provider route. It must come from ${provider}'s getQuote.`);
78
+ }
79
+ if (route.provider !== provider) {
80
+ throw new Error(`Quote came from ${route.provider} but is being swapped on ${provider}.`);
81
+ }
82
+ if (route.quotedFor && route.quotedFor !== userPublicKey.toString()) {
83
+ throw new Error(`Quote was requested for ${route.quotedFor} but is being swapped by ${userPublicKey.toString()}.`);
84
+ }
85
+ assertQuoteMatchesRoute(quote, provider, route.routed);
86
+ return route;
87
+ }
88
+ exports.expectProviderRoute = expectProviderRoute;
@@ -1,36 +1,16 @@
1
- import { Connection, PublicKey, TransactionMessage, AddressLookupTableAccount, TransactionInstruction } from '@solana/web3.js';
2
- import { BN } from '../isomorphic/anchor';
1
+ import { Connection, AddressLookupTableAccount, VersionedTransaction } from '@solana/web3.js';
2
+ import { GetRouteInstructionsParams, SwapProvider, SwapQuote, SwapQuoteParams, SwapRouteInstructions } from '../swap/types';
3
3
  export declare enum SwapMode {
4
4
  ExactIn = "ExactIn",
5
5
  ExactOut = "ExactOut"
6
6
  }
7
- export interface QuoteResponse {
8
- inputMint: string;
9
- inAmount: string;
10
- outputMint: string;
11
- outAmount: string;
12
- swapMode: SwapMode;
13
- slippageBps: number;
14
- platformFee?: {
15
- amount?: string;
16
- feeBps?: number;
17
- };
18
- routePlan: Array<{
19
- swapInfo: any;
20
- percent: number;
21
- }>;
22
- contextSlot?: number;
23
- timeTaken?: number;
24
- error?: string;
25
- errorCode?: string;
26
- }
27
- export declare class TitanClient {
7
+ export declare class TitanClient implements SwapProvider {
8
+ readonly providerName = "titan";
28
9
  authToken: string;
29
10
  url: string;
30
11
  connection: Connection;
31
12
  proxyUrl?: string;
32
- private lastQuoteData?;
33
- private lastQuoteParams?;
13
+ lookupTableCache: Map<string, AddressLookupTableAccount>;
34
14
  constructor({ connection, authToken, url, proxyUrl, }: {
35
15
  connection: Connection;
36
16
  authToken: string;
@@ -39,58 +19,42 @@ export declare class TitanClient {
39
19
  });
40
20
  private buildParams;
41
21
  /**
42
- * Get routes for a swap
22
+ * Get the best available route for a swap.
23
+ *
24
+ * The route is returned on the quote's `providerRoute`, so
25
+ * {@link getRouteInstructions} builds exactly what was quoted here, at the
26
+ * slippage quoted here.
27
+ * @throws If `userPublicKey` is missing — Titan bakes the user's token
28
+ * accounts into the route, so a route quoted for one wallet cannot be
29
+ * executed by another. The wallet is recorded on the route and enforced
30
+ * when the route is built.
43
31
  */
44
- getQuote({ inputMint, outputMint, amount, userPublicKey, maxAccounts, // 50 is an estimated amount with buffer
45
- slippageBps, swapMode, onlyDirectRoutes, excludeDexes, sizeConstraint, accountsLimitWritable, }: {
46
- inputMint: PublicKey;
47
- outputMint: PublicKey;
48
- amount: BN;
49
- userPublicKey: PublicKey;
50
- maxAccounts?: number;
51
- slippageBps?: number;
52
- swapMode?: string;
53
- onlyDirectRoutes?: boolean;
54
- excludeDexes?: string[];
55
- sizeConstraint?: number;
56
- accountsLimitWritable?: number;
57
- }): Promise<QuoteResponse>;
32
+ getQuote({ inputMint, outputMint, amount, userPublicKey, maxAccounts, slippageBps, swapMode, onlyDirectRoutes, excludeDexes, sizeConstraint, accountsLimitWritable, }: SwapQuoteParams): Promise<SwapQuote>;
58
33
  /**
59
- * Get a swap transaction for quote
34
+ * The route as Titan built it, compiled into a signable transaction. Titan
35
+ * returns instructions rather than a transaction, so unlike Jupiter there is
36
+ * nothing to strip — the route already includes its own setup and teardown.
37
+ * @throws If the quote came from a different provider or a different wallet,
38
+ * or if a lookup table the route depends on can't be loaded.
60
39
  */
61
- getSwap({ userPublicKey, }: {
62
- inputMint?: PublicKey;
63
- outputMint?: PublicKey;
64
- amount?: BN;
65
- userPublicKey: PublicKey;
66
- maxAccounts?: number;
67
- slippageBps?: number;
68
- swapMode?: SwapMode;
69
- onlyDirectRoutes?: boolean;
70
- excludeDexes?: string[];
71
- sizeConstraint?: number;
72
- accountsLimitWritable?: number;
73
- }): Promise<{
74
- transactionMessage: TransactionMessage;
75
- lookupTables: AddressLookupTableAccount[];
76
- }>;
40
+ getSwapTransaction({ quote, userPublicKey, }: GetRouteInstructionsParams): Promise<VersionedTransaction>;
77
41
  /**
78
- * Get the titan instructions from transaction by filtering out instructions to compute budget and associated token programs
79
- * @param transactionMessage the transaction message
80
- * @param inputMint the input mint
81
- * @param outputMint the output mint
42
+ * Builds the route instructions for a quote returned by {@link getQuote}.
43
+ *
44
+ * The route travels on the quote, so this reads no client state and two
45
+ * quotes in flight can never be confused for one another. Slippage was
46
+ * fixed when Titan built the route, so there is nothing to apply here.
47
+ * @throws If the quote came from a different provider or a different wallet,
48
+ * or if a lookup table the route depends on can't be loaded.
82
49
  */
83
- getTitanInstructions({ transactionMessage, inputMint, outputMint, }: {
84
- transactionMessage: TransactionMessage;
85
- inputMint: PublicKey;
86
- outputMint: PublicKey;
87
- }): TransactionInstruction[];
50
+ getRouteInstructions({ quote, userPublicKey, }: GetRouteInstructionsParams): Promise<SwapRouteInstructions>;
88
51
  /**
89
52
  * Fetches a lookup table required by a route, retrying transient RPC
90
- * failures (rate limiting in particular) before giving up.
53
+ * failures (rate limiting in particular) before giving up. Checks the
54
+ * instance cache first and populates it on a fresh fetch.
91
55
  * @throws If the table still can't be loaded, or doesn't exist on-chain.
92
56
  */
93
57
  private fetchLookupTable;
94
- private getTransactionMessageAndLookupTables;
58
+ private getInstructionsAndLookupTables;
95
59
  }
96
60
  //# sourceMappingURL=titanClient.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"titanClient.d.ts","sourceRoot":"","sources":["../../../src/titan/titanClient.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"titanClient.d.ts","sourceRoot":"","sources":["../../../src/titan/titanClient.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,UAAU,EAEV,yBAAyB,EAGzB,oBAAoB,EACpB,MAAM,iBAAiB,CAAC;AAIzB,OAAO,EAEN,0BAA0B,EAC1B,YAAY,EACZ,SAAS,EACT,eAAe,EACf,qBAAqB,EAGrB,MAAM,eAAe,CAAC;AAEvB,oBAAY,QAAQ;IACnB,OAAO,YAAY;IACnB,QAAQ,aAAa;CACrB;AA4FD,qBAAa,WAAY,YAAW,YAAY;IAC/C,SAAgB,YAAY,WAAW;IAEvC,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,UAAU,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,yCAAgD;gBAEpD,EACX,UAAU,EACV,SAAS,EACT,GAAG,EACH,QAAQ,GACR,EAAE;QACF,UAAU,EAAE,UAAU,CAAC;QACvB,SAAS,EAAE,MAAM,CAAC;QAClB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE,MAAM,CAAC;KAClB;IAOD,OAAO,CAAC,WAAW;IAwDnB;;;;;;;;;;OAUG;IACU,QAAQ,CAAC,EACrB,SAAS,EACT,UAAU,EACV,MAAM,EACN,aAAa,EACb,WAAuC,EACvC,WAAW,EACX,QAAQ,EACR,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACd,qBAAqB,GACrB,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,CAAC;IA2HvC;;;;;;OAMG;IACU,kBAAkB,CAAC,EAC/B,KAAK,EACL,aAAa,GACb,EAAE,0BAA0B,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAsB7D;;;;;;;;OAQG;IACU,oBAAoB,CAAC,EACjC,KAAK,EACL,aAAa,GACb,EAAE,0BAA0B,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAwB9D;;;;;OAKG;YACW,gBAAgB;YA4ChB,8BAA8B;CA6B5C"}
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TitanClient = exports.SwapMode = void 0;
4
4
  const web3_js_1 = require("@solana/web3.js");
5
5
  const msgpack_1 = require("@msgpack/msgpack");
6
+ const routeInstructions_1 = require("../swap/routeInstructions");
7
+ const types_1 = require("../swap/types");
6
8
  var SwapMode;
7
9
  (function (SwapMode) {
8
10
  SwapMode["ExactIn"] = "ExactIn";
@@ -13,8 +15,18 @@ const TITAN_API_URL = 'https://api.titan.exchange';
13
15
  const LOOKUP_TABLE_FETCH_RETRIES = 2;
14
16
  const LOOKUP_TABLE_RETRY_BASE_DELAY_MS = 150;
15
17
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
18
+ /** Titan sends pubkeys as raw bytes. Absent fields decode to `undefined`. */
19
+ const decodePubkey = (bytes) => bytes ? new web3_js_1.PublicKey(bytes).toString() : undefined;
20
+ /**
21
+ * For the normalized quote's small metadata fields, which are typed `number`.
22
+ * Only safe because slots, durations and bps are far below 2^53 — never use it
23
+ * on a token amount.
24
+ */
25
+ const toNumber = (value) => value === undefined ? undefined : Number(value);
16
26
  class TitanClient {
17
27
  constructor({ connection, authToken, url, proxyUrl, }) {
28
+ this.providerName = 'titan';
29
+ this.lookupTableCache = new Map();
18
30
  this.connection = connection;
19
31
  this.authToken = authToken;
20
32
  this.url = url !== null && url !== void 0 ? url : TITAN_API_URL;
@@ -30,25 +42,41 @@ class TitanClient {
30
42
  outputMint: outputMint.toString(),
31
43
  amount: amount.toString(),
32
44
  userPublicKey: userPublicKey.toString(),
33
- ...(slippageBps && { slippageBps: slippageBps.toString() }),
34
- ...(swapMode && { swapMode: normalizedSwapMode.toString() }),
35
- ...(maxAccounts && { accountsLimitTotal: maxAccounts.toString() }),
36
- ...(excludeDexes && { excludeDexes: excludeDexes.join(',') }),
37
- ...(onlyDirectRoutes && {
45
+ ...(slippageBps != null && { slippageBps: slippageBps.toString() }),
46
+ ...(swapMode != null && { swapMode: normalizedSwapMode.toString() }),
47
+ ...(maxAccounts != null && {
48
+ accountsLimitTotal: maxAccounts.toString(),
49
+ }),
50
+ ...(excludeDexes != null && { excludeDexes: excludeDexes.join(',') }),
51
+ // Only sent when explicitly true — Titan treats the field's presence,
52
+ // not its value, as the toggle.
53
+ ...(onlyDirectRoutes === true && {
38
54
  onlyDirectRoutes: onlyDirectRoutes.toString(),
39
55
  }),
40
- ...(sizeConstraint && { sizeConstraint: sizeConstraint.toString() }),
41
- ...(accountsLimitWritable && {
56
+ ...(sizeConstraint != null && {
57
+ sizeConstraint: sizeConstraint.toString(),
58
+ }),
59
+ ...(accountsLimitWritable != null && {
42
60
  accountsLimitWritable: accountsLimitWritable.toString(),
43
61
  }),
44
62
  });
45
63
  }
46
64
  /**
47
- * Get routes for a swap
65
+ * Get the best available route for a swap.
66
+ *
67
+ * The route is returned on the quote's `providerRoute`, so
68
+ * {@link getRouteInstructions} builds exactly what was quoted here, at the
69
+ * slippage quoted here.
70
+ * @throws If `userPublicKey` is missing — Titan bakes the user's token
71
+ * accounts into the route, so a route quoted for one wallet cannot be
72
+ * executed by another. The wallet is recorded on the route and enforced
73
+ * when the route is built.
48
74
  */
49
- async getQuote({ inputMint, outputMint, amount, userPublicKey, maxAccounts = 50, // 50 is an estimated amount with buffer
50
- slippageBps, swapMode, onlyDirectRoutes, excludeDexes, sizeConstraint, accountsLimitWritable, }) {
51
- var _a;
75
+ async getQuote({ inputMint, outputMint, amount, userPublicKey, maxAccounts = types_1.DEFAULT_SWAP_MAX_ACCOUNTS, slippageBps, swapMode, onlyDirectRoutes, excludeDexes, sizeConstraint, accountsLimitWritable, }) {
76
+ var _a, _b, _c, _d, _e;
77
+ if (!userPublicKey) {
78
+ throw new Error('Titan quotes require a userPublicKey.');
79
+ }
52
80
  const params = this.buildParams({
53
81
  inputMint,
54
82
  outputMint,
@@ -87,29 +115,43 @@ class TitanClient {
87
115
  throw new Error(`Titan API error: ${response.status} ${response.statusText}`);
88
116
  }
89
117
  const buffer = await response.arrayBuffer();
90
- const data = (0, msgpack_1.decode)(buffer);
91
- // Cache the quote data and parameters for later use in getSwap
92
- this.lastQuoteData = data;
93
- this.lastQuoteParams = params.toString();
118
+ // `useBigInt64` or every u64 above 2^53 is silently rounded on the way in,
119
+ // including the `inAmount` callers size `beginSwap` off.
120
+ const data = (0, msgpack_1.decode)(buffer, { useBigInt64: true });
94
121
  // We are only querying for the best avaiable route so use that
95
122
  const route = data.quotes[Object.keys(data.quotes)[0]];
96
123
  if (!route) {
97
124
  throw new Error('No routes available');
98
125
  }
99
- return {
100
- inputMint: inputMint.toString(),
101
- inAmount: amount.toString(),
102
- outputMint: outputMint.toString(),
126
+ if (!((_a = route.instructions) === null || _a === void 0 ? void 0 : _a.length)) {
127
+ throw new Error('Titan route has no instructions');
128
+ }
129
+ // Titan echoes the pair it routed. Take the pair from the response rather
130
+ // than assuming the request was honoured — a route for another pair pays
131
+ // out into a token account `endSwap` isn't watching, and only fails
132
+ // on-chain once the funds have already moved.
133
+ const routedInputMint = (_b = decodePubkey(data.inputMint)) !== null && _b !== void 0 ? _b : inputMint.toString();
134
+ const routedOutputMint = (_c = decodePubkey(data.outputMint)) !== null && _c !== void 0 ? _c : outputMint.toString();
135
+ if (routedInputMint !== inputMint.toString() ||
136
+ routedOutputMint !== outputMint.toString()) {
137
+ throw new Error(`Titan quoted ${routedInputMint} -> ${routedOutputMint} but the swap asked for ${inputMint.toString()} -> ${outputMint.toString()}.`);
138
+ }
139
+ return (0, types_1.buildSwapQuote)({
140
+ inputMint: routedInputMint,
141
+ outputMint: routedOutputMint,
142
+ // The route's own input, not the requested amount — under ExactOut the
143
+ // request is the output, and callers size `beginSwap` off `inAmount`.
144
+ inAmount: ((_d = route.inAmount) !== null && _d !== void 0 ? _d : amount).toString(),
103
145
  outAmount: route.outAmount.toString(),
104
146
  swapMode: data.swapMode,
105
- slippageBps: route.slippageBps,
147
+ slippageBps: Number(route.slippageBps),
106
148
  platformFee: route.platformFee
107
149
  ? {
108
150
  amount: route.platformFee.amount.toString(),
109
- feeBps: route.platformFee.fee_bps,
151
+ feeBps: Number(route.platformFee.fee_bps),
110
152
  }
111
153
  : undefined,
112
- routePlan: ((_a = route.steps) === null || _a === void 0 ? void 0 : _a.map((step) => {
154
+ routePlan: ((_e = route.steps) === null || _e === void 0 ? void 0 : _e.map((step) => {
113
155
  var _a;
114
156
  return ({
115
157
  swapInfo: {
@@ -120,85 +162,81 @@ class TitanClient {
120
162
  inAmount: step.inAmount.toString(),
121
163
  outAmount: step.outAmount.toString(),
122
164
  feeAmount: ((_a = step.feeAmount) === null || _a === void 0 ? void 0 : _a.toString()) || '0',
123
- feeMint: step.feeMint ? new web3_js_1.PublicKey(step.feeMint).toString() : '',
165
+ feeMint: step.feeMint
166
+ ? new web3_js_1.PublicKey(step.feeMint).toString()
167
+ : '',
124
168
  },
125
169
  percent: 100,
126
170
  });
127
171
  })) || [],
128
- contextSlot: route.contextSlot,
129
- timeTaken: route.timeTaken,
130
- };
172
+ contextSlot: toNumber(route.contextSlot),
173
+ timeTaken: toNumber(route.timeTaken),
174
+ }, { provider: 'titan', route, quotedFor: userPublicKey.toString() });
131
175
  }
132
176
  /**
133
- * Get a swap transaction for quote
177
+ * The route as Titan built it, compiled into a signable transaction. Titan
178
+ * returns instructions rather than a transaction, so unlike Jupiter there is
179
+ * nothing to strip — the route already includes its own setup and teardown.
180
+ * @throws If the quote came from a different provider or a different wallet,
181
+ * or if a lookup table the route depends on can't be loaded.
134
182
  */
135
- async getSwap({ userPublicKey, }) {
136
- // Check if we have cached quote data that matches the current parameters
137
- if (!this.lastQuoteData) {
138
- throw new Error('No matching quote data found. Please get a fresh quote before attempting to swap.');
139
- }
140
- // Reuse the cached quote data
141
- const data = this.lastQuoteData;
142
- // We are only querying for the best avaiable route so use that
143
- const route = data.quotes[Object.keys(data.quotes)[0]];
144
- if (!route) {
145
- throw new Error('No routes available');
146
- }
147
- if (route.instructions && route.instructions.length > 0) {
148
- // Errors propagate as-is. Replacing them with generic copy here loses
149
- // the reason the swap can't be built — an unresolvable lookup table,
150
- // say — which the caller needs to decide whether re-quoting will help.
151
- try {
152
- const { transactionMessage, lookupTables } = await this.getTransactionMessageAndLookupTables(route, userPublicKey);
153
- return { transactionMessage, lookupTables };
154
- }
155
- finally {
156
- // Clear cached quote data after use
157
- this.lastQuoteData = undefined;
158
- this.lastQuoteParams = undefined;
159
- }
183
+ async getSwapTransaction({ quote, userPublicKey, }) {
184
+ var _a;
185
+ const route = (0, types_1.expectProviderRoute)(quote, 'titan', userPublicKey)
186
+ .route;
187
+ if (!((_a = route.instructions) === null || _a === void 0 ? void 0 : _a.length)) {
188
+ throw new Error('No instructions provided in the route');
160
189
  }
161
- throw new Error('No instructions provided in the route');
190
+ const [{ instructions, lookupTables }, { blockhash }] = await Promise.all([
191
+ this.getInstructionsAndLookupTables(route),
192
+ this.connection.getLatestBlockhash(),
193
+ ]);
194
+ return new web3_js_1.VersionedTransaction(new web3_js_1.TransactionMessage({
195
+ payerKey: userPublicKey,
196
+ recentBlockhash: blockhash,
197
+ instructions,
198
+ }).compileToV0Message(lookupTables));
162
199
  }
163
200
  /**
164
- * Get the titan instructions from transaction by filtering out instructions to compute budget and associated token programs
165
- * @param transactionMessage the transaction message
166
- * @param inputMint the input mint
167
- * @param outputMint the output mint
201
+ * Builds the route instructions for a quote returned by {@link getQuote}.
202
+ *
203
+ * The route travels on the quote, so this reads no client state and two
204
+ * quotes in flight can never be confused for one another. Slippage was
205
+ * fixed when Titan built the route, so there is nothing to apply here.
206
+ * @throws If the quote came from a different provider or a different wallet,
207
+ * or if a lookup table the route depends on can't be loaded.
168
208
  */
169
- getTitanInstructions({ transactionMessage, inputMint, outputMint, }) {
170
- // Filter out common system instructions that can be handled by VelocityClient
171
- const filteredInstructions = transactionMessage.instructions.filter((instruction) => {
172
- const programId = instruction.programId.toString();
173
- // Filter out system programs
174
- if (programId === 'ComputeBudget111111111111111111111111111111') {
175
- return false;
176
- }
177
- if (programId === 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA') {
178
- return false;
179
- }
180
- if (programId === '11111111111111111111111111111111') {
181
- return false;
182
- }
183
- // Filter out Associated Token Account creation for input/output mints
184
- if (programId === 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL') {
185
- if (instruction.keys.length > 3) {
186
- const mint = instruction.keys[3].pubkey;
187
- if (mint.equals(inputMint) || mint.equals(outputMint)) {
188
- return false;
189
- }
190
- }
191
- }
192
- return true;
193
- });
194
- return filteredInstructions;
209
+ async getRouteInstructions({ quote, userPublicKey, }) {
210
+ var _a;
211
+ const route = (0, types_1.expectProviderRoute)(quote, 'titan', userPublicKey)
212
+ .route;
213
+ if (!((_a = route.instructions) === null || _a === void 0 ? void 0 : _a.length)) {
214
+ throw new Error('No instructions provided in the route');
215
+ }
216
+ // Errors propagate as-is. Replacing them with generic copy here loses
217
+ // the reason the swap can't be built — an unresolvable lookup table,
218
+ // say — which the caller needs to decide whether re-quoting will help.
219
+ const { instructions, lookupTables } = await this.getInstructionsAndLookupTables(route);
220
+ return {
221
+ instructions: (0, routeInstructions_1.filterRouteInstructions)({
222
+ instructions,
223
+ inputMint: new web3_js_1.PublicKey(quote.inputMint),
224
+ outputMint: new web3_js_1.PublicKey(quote.outputMint),
225
+ }),
226
+ lookupTables,
227
+ };
195
228
  }
196
229
  /**
197
230
  * Fetches a lookup table required by a route, retrying transient RPC
198
- * failures (rate limiting in particular) before giving up.
231
+ * failures (rate limiting in particular) before giving up. Checks the
232
+ * instance cache first and populates it on a fresh fetch.
199
233
  * @throws If the table still can't be loaded, or doesn't exist on-chain.
200
234
  */
201
235
  async fetchLookupTable(altPubkey) {
236
+ const cached = this.lookupTableCache.get(altPubkey.toString());
237
+ if (cached !== undefined) {
238
+ return cached;
239
+ }
202
240
  let lastError;
203
241
  for (let attempt = 0; attempt <= LOOKUP_TABLE_FETCH_RETRIES; attempt++) {
204
242
  if (attempt > 0) {
@@ -214,6 +252,7 @@ class TitanClient {
214
252
  continue;
215
253
  }
216
254
  if (altAccount.value) {
255
+ this.lookupTableCache.set(altPubkey.toString(), altAccount.value);
217
256
  return altAccount.value;
218
257
  }
219
258
  // A successful response with no value means the route references a
@@ -222,8 +261,9 @@ class TitanClient {
222
261
  }
223
262
  throw new Error(`Failed to fetch address lookup table ${altPubkey.toString()}: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
224
263
  }
225
- async getTransactionMessageAndLookupTables(route, userPublicKey) {
226
- const solanaInstructions = route.instructions.map((instruction) => ({
264
+ async getInstructionsAndLookupTables(route) {
265
+ var _a;
266
+ const instructions = route.instructions.map((instruction) => ({
227
267
  programId: new web3_js_1.PublicKey(instruction.p),
228
268
  keys: instruction.a.map((meta) => ({
229
269
  pubkey: new web3_js_1.PublicKey(meta.p),
@@ -232,27 +272,13 @@ class TitanClient {
232
272
  })),
233
273
  data: Buffer.from(instruction.d),
234
274
  }));
235
- // Get recent blockhash
236
- const { blockhash } = await this.connection.getLatestBlockhash();
237
- // Build address lookup tables if provided.
238
- //
239
275
  // These all have to resolve. A table that fails to load isn't a slightly
240
276
  // worse route — every account it would have compressed to a 1-byte index
241
277
  // gets inlined as a 32-byte pubkey instead, which pushes the transaction
242
278
  // past the size limit and only surfaces later as an opaque
243
279
  // "encoding overruns Uint8Array". Failing here lets the caller re-quote.
244
- const addressLookupTables = [];
245
- if (route.addressLookupTables && route.addressLookupTables.length > 0) {
246
- for (const altPubkey of route.addressLookupTables) {
247
- addressLookupTables.push(await this.fetchLookupTable(new web3_js_1.PublicKey(altPubkey)));
248
- }
249
- }
250
- const transactionMessage = new web3_js_1.TransactionMessage({
251
- payerKey: userPublicKey,
252
- recentBlockhash: blockhash,
253
- instructions: solanaInstructions,
254
- });
255
- return { transactionMessage, lookupTables: addressLookupTables };
280
+ const lookupTables = await Promise.all(((_a = route.addressLookupTables) !== null && _a !== void 0 ? _a : []).map((altPubkey) => this.fetchLookupTable(new web3_js_1.PublicKey(altPubkey))));
281
+ return { instructions, lookupTables };
256
282
  }
257
283
  }
258
284
  exports.TitanClient = TitanClient;
@@ -410,14 +410,15 @@ export declare class User {
410
410
  */
411
411
  getTotalCollateral(marginCategory?: MarginCategory, strict?: boolean, includeOpenOrders?: boolean, liquidationBuffer?: BN, perpMarketIndex?: number): BN;
412
412
  /**
413
- * True when the account has an admin-set `equityFloor` and its cross-margin
414
- * total collateral is below it. This is the trip threshold of the
415
- * permissionless `tripEquityFloorBreaker`; action gating happens at
416
- * `equityFloor + equityFloorBuffer` (see `isBelowBufferedEquityFloor`).
417
- * Mirrors `User::is_below_equity_floor` on-chain.
418
- * @param strict Use TWAP-bounded oracle pricing, matching the withdraw path. Defaults to false.
413
+ * True when the account has an admin-set `equityFloor` and its net equity
414
+ * (`getNetUsdValue`: unweighted assets and perp PnL minus unweighted spot
415
+ * liabilities, at live oracle prices) is below it. This is the trip
416
+ * threshold of the permissionless `tripEquityFloorBreaker`; action gating
417
+ * happens at `equityFloor + equityFloorBuffer` (see
418
+ * `isBelowBufferedEquityFloor`). Mirrors `User::is_below_equity_floor`
419
+ * onchain.
419
420
  */
420
- isBelowEquityFloor(strict?: boolean): boolean;
421
+ isBelowEquityFloor(): boolean;
421
422
  /**
422
423
  * The equity required by risk-increasing actions:
423
424
  * `equityFloor + equityFloorBuffer` (QUOTE_PRECISION). Mirrors
@@ -426,31 +427,28 @@ export declare class User {
426
427
  */
427
428
  getBufferedEquityFloor(): BN;
428
429
  /**
429
- * True when the account has an admin-set `equityFloor` and its cross-margin
430
- * total collateral is below `equityFloor + equityFloorBuffer`. While below,
431
- * the program rejects risk-increasing order placement and fills,
430
+ * True when the account has an admin-set `equityFloor` and its net equity
431
+ * (`getNetUsdValue`) is below `equityFloor + equityFloorBuffer`. While
432
+ * below, the program rejects risk-increasing order placement and fills,
432
433
  * withdrawals, and transfers out of the account (`EquityBelowFloor`);
433
434
  * reduce-only activity stays allowed. Mirrors
434
435
  * `User::is_below_buffered_equity_floor` on-chain.
435
- * @param strict Use TWAP-bounded oracle pricing, matching the withdraw path. Defaults to false.
436
436
  */
437
- isBelowBufferedEquityFloor(strict?: boolean): boolean;
437
+ isBelowBufferedEquityFloor(): boolean;
438
438
  /**
439
- * Cross-margin total collateral in excess of the admin-set `equityFloor`,
439
+ * Net equity (`getNetUsdValue`) in excess of the admin-set `equityFloor`,
440
440
  * floored at zero (QUOTE_PRECISION). Unbounded (`null`) when no floor is set.
441
441
  * This is headroom above the trip threshold; headroom above the level
442
442
  * risk-increasing actions must clear is `getEquityAboveBufferedFloor`.
443
- * @param strict Use TWAP-bounded oracle pricing. Defaults to false.
444
443
  */
445
- getEquityAboveFloor(strict?: boolean): BN | null;
444
+ getEquityAboveFloor(): BN | null;
446
445
  /**
447
- * Cross-margin total collateral in excess of `equityFloor +
446
+ * Net equity (`getNetUsdValue`) in excess of `equityFloor +
448
447
  * equityFloorBuffer`, floored at zero (QUOTE_PRECISION). Unbounded
449
448
  * (`null`) when no floor is set. When this reaches zero, risk-increasing
450
449
  * actions start rejecting.
451
- * @param strict Use TWAP-bounded oracle pricing. Defaults to false.
452
450
  */
453
- getEquityAboveBufferedFloor(strict?: boolean): BN | null;
451
+ getEquityAboveBufferedFloor(): BN | null;
454
452
  /**
455
453
  * Builds the liquidation-buffer map to pass into margin calculations while
456
454
  * a liquidation is in progress: `'cross'` is set to the state account's