@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.
- package/lib/browser/adminClient.d.ts +2 -2
- package/lib/browser/adminClient.js +3 -11
- package/lib/browser/equityFloorManager.d.ts +9 -5
- package/lib/browser/equityFloorManager.js +9 -5
- package/lib/browser/idl/velocity.d.ts +2 -1
- package/lib/browser/idl/velocity.json +2 -1
- package/lib/browser/index.d.ts +2 -0
- package/lib/browser/index.js +3 -0
- package/lib/browser/jupiter/jupiterClient.d.ts +31 -27
- package/lib/browser/jupiter/jupiterClient.js +61 -35
- package/lib/browser/math/margin.d.ts +12 -10
- package/lib/browser/math/margin.js +16 -14
- package/lib/browser/math/superStake.d.ts +9 -9
- package/lib/browser/math/superStake.js +8 -8
- package/lib/browser/swap/UnifiedSwapClient.d.ts +38 -76
- package/lib/browser/swap/UnifiedSwapClient.js +46 -117
- package/lib/browser/swap/routeInstructions.d.ts +19 -0
- package/lib/browser/swap/routeInstructions.js +43 -0
- package/lib/browser/swap/types.d.ts +196 -0
- package/lib/browser/swap/types.js +88 -0
- package/lib/browser/titan/titanClient.d.ts +32 -68
- package/lib/browser/titan/titanClient.js +128 -102
- package/lib/browser/user.d.ts +16 -18
- package/lib/browser/user.js +24 -26
- package/lib/browser/velocityClient.d.ts +83 -101
- package/lib/browser/velocityClient.js +152 -289
- package/lib/node/adminClient.d.ts +2 -2
- package/lib/node/adminClient.d.ts.map +1 -1
- package/lib/node/adminClient.js +3 -11
- package/lib/node/equityFloorManager.d.ts +9 -5
- package/lib/node/equityFloorManager.d.ts.map +1 -1
- package/lib/node/equityFloorManager.js +9 -5
- package/lib/node/idl/velocity.d.ts +2 -1
- package/lib/node/idl/velocity.d.ts.map +1 -1
- package/lib/node/idl/velocity.json +2 -1
- package/lib/node/index.d.ts +2 -0
- package/lib/node/index.d.ts.map +1 -1
- package/lib/node/index.js +3 -0
- package/lib/node/jupiter/jupiterClient.d.ts +31 -27
- package/lib/node/jupiter/jupiterClient.d.ts.map +1 -1
- package/lib/node/jupiter/jupiterClient.js +61 -35
- package/lib/node/math/margin.d.ts +12 -10
- package/lib/node/math/margin.d.ts.map +1 -1
- package/lib/node/math/margin.js +16 -14
- package/lib/node/math/superStake.d.ts +9 -9
- package/lib/node/math/superStake.d.ts.map +1 -1
- package/lib/node/math/superStake.js +8 -8
- package/lib/node/swap/UnifiedSwapClient.d.ts +38 -76
- package/lib/node/swap/UnifiedSwapClient.d.ts.map +1 -1
- package/lib/node/swap/UnifiedSwapClient.js +46 -117
- package/lib/node/swap/routeInstructions.d.ts +20 -0
- package/lib/node/swap/routeInstructions.d.ts.map +1 -0
- package/lib/node/swap/routeInstructions.js +43 -0
- package/lib/node/swap/types.d.ts +197 -0
- package/lib/node/swap/types.d.ts.map +1 -0
- package/lib/node/swap/types.js +88 -0
- package/lib/node/titan/titanClient.d.ts +32 -68
- package/lib/node/titan/titanClient.d.ts.map +1 -1
- package/lib/node/titan/titanClient.js +128 -102
- package/lib/node/user.d.ts +16 -18
- package/lib/node/user.d.ts.map +1 -1
- package/lib/node/user.js +24 -26
- package/lib/node/velocityClient.d.ts +83 -101
- package/lib/node/velocityClient.d.ts.map +1 -1
- package/lib/node/velocityClient.js +152 -289
- 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,
|
|
2
|
-
import {
|
|
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
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
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
|
-
|
|
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
|
|
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":"AAAA,OAAO,EACN,UAAU,
|
|
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 && {
|
|
36
|
-
|
|
37
|
-
|
|
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 && {
|
|
41
|
-
|
|
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
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
91
|
-
//
|
|
92
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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: ((
|
|
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
|
|
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
|
-
*
|
|
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
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
226
|
-
|
|
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
|
|
245
|
-
|
|
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;
|
package/lib/node/user.d.ts
CHANGED
|
@@ -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
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
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(
|
|
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
|
|
430
|
-
*
|
|
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(
|
|
437
|
+
isBelowBufferedEquityFloor(): boolean;
|
|
438
438
|
/**
|
|
439
|
-
*
|
|
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(
|
|
444
|
+
getEquityAboveFloor(): BN | null;
|
|
446
445
|
/**
|
|
447
|
-
*
|
|
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(
|
|
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
|