@shogun-sdk/intents-sdk 1.4.2 → 1.4.3
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/dist/index.cjs +246 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -2
- package/dist/index.d.ts +58 -2
- package/dist/index.js +246 -102
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- package/src/core/orders/cross-chain.ts +5 -1
- package/src/core/orders/single-chain.ts +11 -21
- package/src/index.ts +10 -1
- package/src/utils/quote/aggregator.ts +187 -58
- package/src/utils/quote/pricing/constants.ts +50 -6
- package/src/utils/quote/pricing/expenses.ts +78 -22
- package/src/utils/quote/pricing/gas.ts +71 -6
- package/src/utils/quote/pricing/limit-amount.ts +21 -0
- package/src/utils/quote/raydium.ts +1 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shogun-sdk/intents-sdk",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Shogun Network Intent-based cross-chain swaps SDK",
|
|
6
6
|
"author": "Shogun network",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"@types/node": "22.13.10",
|
|
49
49
|
"codama": "1.3.4",
|
|
50
50
|
"tsup": "^8.5.0",
|
|
51
|
+
"tsx": "^4.22.4",
|
|
51
52
|
"typescript": "5.8.2",
|
|
52
53
|
"vitest": "^2.1.9"
|
|
53
54
|
},
|
|
@@ -92,6 +93,7 @@
|
|
|
92
93
|
"pre-publish": "pnpm node ./scripts/prePublish.js",
|
|
93
94
|
"generate-solana-idl": "pnpm node ./scripts/codama.js",
|
|
94
95
|
"test": "vitest run",
|
|
95
|
-
"test:watch": "vitest"
|
|
96
|
+
"test:watch": "vitest",
|
|
97
|
+
"smoke:quote": "tsx scripts/net-quote-smoke.ts"
|
|
96
98
|
}
|
|
97
99
|
}
|
|
@@ -39,6 +39,8 @@ export type CreateCrossChainOrderParams = {
|
|
|
39
39
|
deadline: number;
|
|
40
40
|
/** Extra transfers to be made */
|
|
41
41
|
extraTransfers?: ExtraTransfer[];
|
|
42
|
+
/** Slippage tolerance in bps for the auto-quote when min amounts are not provided. Defaults to DEFAULT_CROSS_CHAIN_SLIPPAGE_BPS (25) per swap leg. */
|
|
43
|
+
slippageBps?: number;
|
|
42
44
|
|
|
43
45
|
/** Stop loss */
|
|
44
46
|
stopLossType?: StopLossType;
|
|
@@ -168,12 +170,14 @@ export class CrossChainOrder {
|
|
|
168
170
|
|
|
169
171
|
// TODO: Change this
|
|
170
172
|
if (!minStablecoinAmount || !destinationTokenMinAmount) {
|
|
171
|
-
const quote = await QuoteProvider.
|
|
173
|
+
const quote = await QuoteProvider.getCrossChainNetQuote({
|
|
172
174
|
sourceChainId: input.sourceChainId,
|
|
173
175
|
tokenIn: input.sourceTokenAddress,
|
|
174
176
|
amount: input.sourceTokenAmount,
|
|
175
177
|
destChainId: input.destinationChainId,
|
|
176
178
|
tokenOut: input.destinationTokenAddress,
|
|
179
|
+
extraTransfers: input.extraTransfers,
|
|
180
|
+
slippageBps: input.slippageBps,
|
|
177
181
|
});
|
|
178
182
|
|
|
179
183
|
return {
|
|
@@ -4,9 +4,6 @@ import type { ApiResponse } from '../../types/api.js';
|
|
|
4
4
|
import type { SingleChainPreparedData, SingleChainUserIntentRequest } from '../../types/intent.js';
|
|
5
5
|
import { SingleChainOrderValidator } from '../../utils/order-validator.js';
|
|
6
6
|
import { QuoteProvider } from '../../utils/quote/aggregator.js';
|
|
7
|
-
import { estimateAmountOut } from '../../utils/quote/pricing/estimate-amount-out.js';
|
|
8
|
-
import { estimateExpensesInTokenOut } from '../../utils/quote/pricing/expenses.js';
|
|
9
|
-
import { DEFAULT_COMMISSION_BPS, EVM_SINGLE_CHAIN_GAS_LIMIT, SINGLE_CHAIN_COMMISSION_DENOMINATOR } from '../../utils/quote/pricing/constants.js';
|
|
10
7
|
import { getEVMSingleChainOrderTypedData } from '../evm/order-signature.js';
|
|
11
8
|
import { BaseSDK } from '../sdk.js';
|
|
12
9
|
import { getSolanaSingleChainOrderInstructions } from '../solana/order-instructions.js';
|
|
@@ -21,6 +18,8 @@ export type CreateSingleChainOrderParams = {
|
|
|
21
18
|
destinationAddress: string;
|
|
22
19
|
extraTransfers?: ExtraTransfer[];
|
|
23
20
|
deadline: number;
|
|
21
|
+
/** Slippage tolerance in bps for the auto-quote when amountOutMin is not provided. Defaults to DEFAULT_SINGLE_CHAIN_SLIPPAGE_BPS (50). */
|
|
22
|
+
slippageBps?: number;
|
|
24
23
|
|
|
25
24
|
stopLossType?: StopLossType;
|
|
26
25
|
stopLossTriggerPrice?: string;
|
|
@@ -119,24 +118,15 @@ export class SingleChainOrder {
|
|
|
119
118
|
|
|
120
119
|
switch (scenario) {
|
|
121
120
|
case 'QUOTE_REQUIRED': {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
tokenOut: input.tokenOut,
|
|
132
|
-
gasLimit: EVM_SINGLE_CHAIN_GAS_LIMIT,
|
|
133
|
-
getQuote: (p) => QuoteProvider.getSingleChainQuote(p),
|
|
134
|
-
}),
|
|
135
|
-
]);
|
|
136
|
-
|
|
137
|
-
// Worst-case output already carries provider slippage; never use the nominal amountOut.
|
|
138
|
-
const swapOutput = quote.amountOutMin ?? quote.amountOut;
|
|
139
|
-
return estimateAmountOut(swapOutput, DEFAULT_COMMISSION_BPS, expenses, SINGLE_CHAIN_COMMISSION_DENOMINATOR);
|
|
121
|
+
const { netAmountOut } = await QuoteProvider.getSingleChainNetQuote({
|
|
122
|
+
chainId: input.chainId,
|
|
123
|
+
amount: input.amountIn,
|
|
124
|
+
tokenIn: input.tokenIn,
|
|
125
|
+
tokenOut: input.tokenOut,
|
|
126
|
+
extraTransfers: input.extraTransfers,
|
|
127
|
+
slippageBps: input.slippageBps,
|
|
128
|
+
});
|
|
129
|
+
return netAmountOut;
|
|
140
130
|
}
|
|
141
131
|
|
|
142
132
|
case 'USE_PROVIDED_AMOUNT':
|
package/src/index.ts
CHANGED
|
@@ -77,7 +77,16 @@ export { getSolanaDcaSingleChainOrderInstructions } from './core/solana/dca/crea
|
|
|
77
77
|
export { signSingleChainDcaOrder } from './core/evm/single-chain-dca-order.js';
|
|
78
78
|
export { signSingleChainLimitOrder } from './core/evm/single-chain-limit-order.js';
|
|
79
79
|
|
|
80
|
-
export {
|
|
80
|
+
export {
|
|
81
|
+
QuoteProvider,
|
|
82
|
+
type IntentsQuoteParams,
|
|
83
|
+
type QuoteResponse,
|
|
84
|
+
type Quote,
|
|
85
|
+
type SingleChainNetQuote,
|
|
86
|
+
type SingleChainNetQuoteParams,
|
|
87
|
+
} from './utils/quote/aggregator.js';
|
|
88
|
+
export { IntentNotViableError, type Expenses } from './utils/quote/pricing/estimate-amount-out.js';
|
|
89
|
+
export { type ExtraTransferAmount } from './utils/quote/pricing/expenses.js';
|
|
81
90
|
|
|
82
91
|
export { CROSS_CHAIN_GUARD_ADDRESSES, SINGLE_CHAIN_GUARD_ADDRESSES, PERMIT2_ADDRESS } from './constants.js';
|
|
83
92
|
|
|
@@ -10,9 +10,16 @@ import { RaydiumQuoteProvider, type RaydiumPumpQuoteParams, type RaydiumQuoteRes
|
|
|
10
10
|
import { PumpFunQuoteProvider } from './pumpfun/index.js';
|
|
11
11
|
import { RelayQuoteProvider } from './relay.js';
|
|
12
12
|
import { convertDecimals } from './pricing/decimals.js';
|
|
13
|
-
import { estimateAmountOut, IntentNotViableError } from './pricing/estimate-amount-out.js';
|
|
14
|
-
import { estimateExpensesInTokenOut } from './pricing/expenses.js';
|
|
15
|
-
import {
|
|
13
|
+
import { estimateAmountOut, IntentNotViableError, type Expenses } from './pricing/estimate-amount-out.js';
|
|
14
|
+
import { estimateExpensesInTokenOut, type ExtraTransferAmount } from './pricing/expenses.js';
|
|
15
|
+
import { exactInLimitAmount } from './pricing/limit-amount.js';
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_COMMISSION_BPS,
|
|
18
|
+
CROSS_CHAIN_COMMISSION_DENOMINATOR,
|
|
19
|
+
SINGLE_CHAIN_COMMISSION_DENOMINATOR,
|
|
20
|
+
DEFAULT_SINGLE_CHAIN_SLIPPAGE_BPS,
|
|
21
|
+
DEFAULT_CROSS_CHAIN_SLIPPAGE_BPS,
|
|
22
|
+
} from './pricing/constants.js';
|
|
16
23
|
import { compareAddresses } from './address.js';
|
|
17
24
|
|
|
18
25
|
type SingleChainQuoteParams = {
|
|
@@ -51,6 +58,9 @@ export type IntentsQuoteParams = {
|
|
|
51
58
|
amount: bigint;
|
|
52
59
|
tokenIn: string;
|
|
53
60
|
tokenOut: string;
|
|
61
|
+
extraTransfers?: ExtraTransferAmount[];
|
|
62
|
+
/** Slippage tolerance in basis points, applied to both swap legs. Defaults to DEFAULT_CROSS_CHAIN_SLIPPAGE_BPS (25). */
|
|
63
|
+
slippageBps?: number;
|
|
54
64
|
};
|
|
55
65
|
|
|
56
66
|
export type QuoteResponseInternal = {
|
|
@@ -79,29 +89,110 @@ export type QuoteResponse = QuoteResponseInternal & {
|
|
|
79
89
|
};
|
|
80
90
|
};
|
|
81
91
|
|
|
92
|
+
export type SingleChainNetQuoteParams = SingleChainQuoteParams & {
|
|
93
|
+
extraTransfers?: ExtraTransferAmount[];
|
|
94
|
+
protocolFeeInTokenOut?: bigint;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** A single-chain quote plus the net amount the user receives after gas, extra transfers and commission. */
|
|
98
|
+
export type SingleChainNetQuote = {
|
|
99
|
+
quote: Quote;
|
|
100
|
+
netAmountOut: bigint;
|
|
101
|
+
expenses: Expenses;
|
|
102
|
+
};
|
|
103
|
+
|
|
82
104
|
export class QuoteProvider {
|
|
105
|
+
/**
|
|
106
|
+
* Gross cross-chain quote: two nominal swap legs joined by the bridge stablecoin,
|
|
107
|
+
* with no solver expense model (no gas, commission or amount_limit haircuts).
|
|
108
|
+
* For the amount the solver will actually accept as amountOutMin, use
|
|
109
|
+
* getCrossChainNetQuote instead.
|
|
110
|
+
*/
|
|
83
111
|
public static async getQuote(params: IntentsQuoteParams): Promise<QuoteResponse> {
|
|
84
112
|
try {
|
|
85
|
-
|
|
86
|
-
// Adapt QuoteResponseInternal to QuoteResponse by adding tokenIn and tokenOut metadata fields
|
|
87
|
-
return {
|
|
88
|
-
...quote,
|
|
89
|
-
tokenIn: {
|
|
90
|
-
address: params.tokenIn,
|
|
91
|
-
chainId: params.sourceChainId,
|
|
92
|
-
},
|
|
93
|
-
tokenOut: {
|
|
94
|
-
address: params.tokenOut,
|
|
95
|
-
chainId: params.destChainId,
|
|
96
|
-
},
|
|
97
|
-
};
|
|
113
|
+
return this.adaptQuoteResponse(await this.getGrossQuoteFromRouters(params), params);
|
|
98
114
|
} catch (e) {
|
|
99
|
-
if (e instanceof IntentNotViableError) throw e;
|
|
100
115
|
console.error('Error getting quote from routers:', e);
|
|
101
116
|
throw new Error('Failed to get quote from routers');
|
|
102
117
|
}
|
|
103
118
|
}
|
|
104
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Net cross-chain quote mirroring the solver's estimation exactly: swap legs credited
|
|
122
|
+
* at amount_limit, source/dest gas, commission and extra transfers subtracted from the
|
|
123
|
+
* bridged stablecoin amount. estimatedAmountOutReduced is the max amountOutMin the
|
|
124
|
+
* solver will accept; throws IntentNotViableError when expenses exceed a leg.
|
|
125
|
+
*/
|
|
126
|
+
public static async getCrossChainNetQuote(params: IntentsQuoteParams): Promise<QuoteResponse> {
|
|
127
|
+
try {
|
|
128
|
+
return this.adaptQuoteResponse(await this.getNetQuoteFromRouters(params), params);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
if (e instanceof IntentNotViableError) throw e;
|
|
131
|
+
console.error('Error getting net quote from routers:', e);
|
|
132
|
+
throw new Error('Failed to get net quote from routers');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Adds the tokenIn/tokenOut metadata fields QuoteResponse carries on top of the internal shape. */
|
|
137
|
+
private static adaptQuoteResponse(quote: QuoteResponseInternal, params: IntentsQuoteParams): QuoteResponse {
|
|
138
|
+
return {
|
|
139
|
+
...quote,
|
|
140
|
+
tokenIn: {
|
|
141
|
+
address: params.tokenIn,
|
|
142
|
+
chainId: params.sourceChainId,
|
|
143
|
+
},
|
|
144
|
+
tokenOut: {
|
|
145
|
+
address: params.tokenOut,
|
|
146
|
+
chainId: params.destChainId,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The pre-solver-parity quote path: nominal source leg, decimal-normalized bridge,
|
|
153
|
+
* nominal dest leg. estimatedAmountOutReduced only reflects the dest provider's own
|
|
154
|
+
* amountOutMin when it reports one.
|
|
155
|
+
*/
|
|
156
|
+
private static async getGrossQuoteFromRouters(params: IntentsQuoteParams): Promise<QuoteResponseInternal> {
|
|
157
|
+
const sourceStable = getStableForChain(params.sourceChainId);
|
|
158
|
+
const destStable = getStableForChain(params.destChainId);
|
|
159
|
+
|
|
160
|
+
// Step 1: quote tokenIn -> bridge stablecoin on the source chain
|
|
161
|
+
const sourceQuote = await this.getSingleChainQuote({
|
|
162
|
+
chainId: params.sourceChainId,
|
|
163
|
+
amount: params.amount,
|
|
164
|
+
tokenIn: params.tokenIn,
|
|
165
|
+
tokenOut: sourceStable.address,
|
|
166
|
+
slippageBps: params.slippageBps,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Step 2: normalize decimals between source and destination stablecoins
|
|
170
|
+
const normalizedBridgeAmount = convertDecimals(sourceQuote.amountOut, sourceStable.decimals, destStable.decimals);
|
|
171
|
+
|
|
172
|
+
// Step 3: quote bridge stablecoin -> target token on the dest chain
|
|
173
|
+
const destQuote = await this.getSingleChainQuote({
|
|
174
|
+
chainId: params.destChainId,
|
|
175
|
+
amount: normalizedBridgeAmount,
|
|
176
|
+
tokenIn: destStable.address,
|
|
177
|
+
tokenOut: params.tokenOut,
|
|
178
|
+
slippageBps: params.slippageBps,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Step 4: stablecoin min-amount from the source leg's USD value
|
|
182
|
+
const estimatedAmountInAsMinStablecoinAmount = BigInt(
|
|
183
|
+
Math.floor(sourceQuote.amountInUsd * 10 ** sourceStable.decimals),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
estimatedAmountOut: destQuote.amountOut,
|
|
188
|
+
estimatedAmountOutUsd: destQuote.amountOutUsd,
|
|
189
|
+
amountInUsd: sourceQuote.amountInUsd,
|
|
190
|
+
estimatedAmountInAsMinStablecoinAmount,
|
|
191
|
+
estimatedAmountOutReduced: destQuote.amountOutMin || destQuote.amountOut,
|
|
192
|
+
estimatedAmountOutUsdReduced: destQuote.amountOutUsd,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
105
196
|
/**
|
|
106
197
|
* Get a quote for Solana with explicit provider selection
|
|
107
198
|
* @param params - Quote parameters including provider preference
|
|
@@ -126,58 +217,77 @@ export class QuoteProvider {
|
|
|
126
217
|
return this.getSingleChainQuote(singleChainParams);
|
|
127
218
|
}
|
|
128
219
|
|
|
129
|
-
private static async
|
|
220
|
+
private static async getNetQuoteFromRouters(params: IntentsQuoteParams): Promise<QuoteResponseInternal> {
|
|
130
221
|
const sourceStable = getStableForChain(params.sourceChainId);
|
|
131
222
|
const destStable = getStableForChain(params.destChainId);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
223
|
+
const getQuote = (p: SingleChainQuoteParams) => this.getSingleChainQuote(p);
|
|
224
|
+
const sourceIsStable = compareAddresses(params.tokenIn, sourceStable.address);
|
|
225
|
+
const destIsStable = compareAddresses(params.tokenOut, destStable.address);
|
|
226
|
+
|
|
227
|
+
// Steps 1-2 & 4 inputs are independent (source quote, source-leg gas, dest-leg gas + extra
|
|
228
|
+
// transfers depend only on params), so fetch them concurrently; only the dest swap must wait.
|
|
229
|
+
const [sourceQuote, sourceExpenses, destExpenses] = await Promise.all([
|
|
230
|
+
// Step 1: source quote tokenIn -> source stablecoin
|
|
231
|
+
this.getSingleChainQuote({
|
|
232
|
+
chainId: params.sourceChainId,
|
|
233
|
+
amount: params.amount,
|
|
234
|
+
tokenIn: params.tokenIn,
|
|
235
|
+
tokenOut: sourceStable.address,
|
|
236
|
+
slippageBps: params.slippageBps ?? DEFAULT_CROSS_CHAIN_SLIPPAGE_BPS,
|
|
237
|
+
}),
|
|
238
|
+
// Step 2 input: source-chain gas valued in the source stablecoin. A stablecoin
|
|
239
|
+
// token_in needs no source swap, so the solver charges start_without_swap gas.
|
|
240
|
+
estimateExpensesInTokenOut({
|
|
241
|
+
chainId: params.sourceChainId,
|
|
242
|
+
tokenOut: sourceStable.address,
|
|
243
|
+
getQuote,
|
|
244
|
+
flow: sourceIsStable ? 'crossChainSourceNoSwap' : 'crossChainSource',
|
|
245
|
+
}),
|
|
246
|
+
// Step 4 input: dest gas + extra transfers valued in the dest stablecoin
|
|
143
247
|
estimateExpensesInTokenOut({
|
|
144
248
|
chainId: params.destChainId,
|
|
145
|
-
tokenOut:
|
|
146
|
-
|
|
147
|
-
|
|
249
|
+
tokenOut: destStable.address,
|
|
250
|
+
getQuote,
|
|
251
|
+
extraTransfers: params.extraTransfers,
|
|
252
|
+
flow: 'crossChainDest',
|
|
148
253
|
}),
|
|
149
254
|
]);
|
|
150
255
|
|
|
151
|
-
// Step 2:
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
256
|
+
// Step 2: subtract source-chain gas from the source leg, crediting the swap at
|
|
257
|
+
// amount_limit like the solver (cross_chain_estimation.rs:155). When token_in is
|
|
258
|
+
// already the stablecoin the solver's router short-circuits 1:1 (SimpleTransfer,
|
|
259
|
+
// estimate.rs:141-157) with no slippage haircut.
|
|
260
|
+
// commission 0 here: the solver applies commission only on the destination leg
|
|
261
|
+
const sourceStableOut = sourceIsStable ? sourceQuote.amountOut : exactInLimitAmount(sourceQuote.amountOut);
|
|
262
|
+
const sourceStableAfterGas = estimateAmountOut(sourceStableOut, 0n, sourceExpenses);
|
|
263
|
+
|
|
264
|
+
// Step 3: normalize decimals between source and destination stablecoins
|
|
265
|
+
const normalizedBridgeAmount = convertDecimals(sourceStableAfterGas, sourceStable.decimals, destStable.decimals);
|
|
266
|
+
|
|
267
|
+
// Step 4: subtract commission + dest gas + extra transfers on the bridged stablecoin amount
|
|
268
|
+
const bridgedNet = estimateAmountOut(
|
|
269
|
+
normalizedBridgeAmount,
|
|
270
|
+
DEFAULT_COMMISSION_BPS,
|
|
271
|
+
destExpenses,
|
|
272
|
+
CROSS_CHAIN_COMMISSION_DENOMINATOR,
|
|
157
273
|
);
|
|
158
274
|
|
|
159
|
-
// Step
|
|
160
|
-
const
|
|
275
|
+
// Step 5: swap the net bridged stablecoin amount into the target token
|
|
276
|
+
const destQuote = await this.getSingleChainQuote({
|
|
161
277
|
chainId: params.destChainId,
|
|
162
|
-
amount:
|
|
278
|
+
amount: bridgedNet,
|
|
163
279
|
tokenIn: destStable.address,
|
|
164
280
|
tokenOut: params.tokenOut,
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const destQuote = await this.getSingleChainQuote(destSingleChainQuoteParams);
|
|
281
|
+
slippageBps: params.slippageBps ?? DEFAULT_CROSS_CHAIN_SLIPPAGE_BPS,
|
|
282
|
+
});
|
|
168
283
|
|
|
169
|
-
// Step
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
);
|
|
284
|
+
// Step 6: stablecoin min-amount — the solver accepts a --min-stablecoins floor only up
|
|
285
|
+
// to its own source-swap amount_limit (ws/helpers/mod.rs:32-51), so expose exactly that.
|
|
286
|
+
const estimatedAmountInAsMinStablecoinAmount = sourceStableOut;
|
|
173
287
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
DEFAULT_COMMISSION_BPS,
|
|
178
|
-
expenses,
|
|
179
|
-
CROSS_CHAIN_COMMISSION_DENOMINATOR,
|
|
180
|
-
);
|
|
288
|
+
// The solver's max deliverable is the dest swap's amount_limit (cross_chain_estimation.rs:243),
|
|
289
|
+
// 1:1 when token_out is the stablecoin itself (SimpleTransfer short-circuit).
|
|
290
|
+
const estimatedAmountOutReduced = destIsStable ? destQuote.amountOut : exactInLimitAmount(destQuote.amountOut);
|
|
181
291
|
|
|
182
292
|
return {
|
|
183
293
|
estimatedAmountOut: destQuote.amountOut,
|
|
@@ -270,6 +380,25 @@ export class QuoteProvider {
|
|
|
270
380
|
throw new Error(`Unsupported chain for quote: ${params.chainId}`);
|
|
271
381
|
}
|
|
272
382
|
|
|
383
|
+
public static async getSingleChainNetQuote(params: SingleChainNetQuoteParams): Promise<SingleChainNetQuote> {
|
|
384
|
+
const [quote, expenses] = await Promise.all([
|
|
385
|
+
this.getSingleChainQuote(params),
|
|
386
|
+
estimateExpensesInTokenOut({
|
|
387
|
+
chainId: params.chainId,
|
|
388
|
+
tokenOut: params.tokenOut,
|
|
389
|
+
getQuote: (p) => this.getSingleChainQuote(p),
|
|
390
|
+
extraTransfers: params.extraTransfers,
|
|
391
|
+
protocolFeeInTokenOut: params.protocolFeeInTokenOut,
|
|
392
|
+
flow: 'singleChain',
|
|
393
|
+
}),
|
|
394
|
+
]);
|
|
395
|
+
// The solver credits the swap at amount_limit (nominal minus DEFAULT_SLIPPAGE, rounded
|
|
396
|
+
// up), regardless of any amountOutMin our provider reports — mirror that exactly.
|
|
397
|
+
const swapOutput = exactInLimitAmount(quote.amountOut);
|
|
398
|
+
const netAmountOut = estimateAmountOut(swapOutput, DEFAULT_COMMISSION_BPS, expenses, SINGLE_CHAIN_COMMISSION_DENOMINATOR);
|
|
399
|
+
return { quote, netAmountOut, expenses };
|
|
400
|
+
}
|
|
401
|
+
|
|
273
402
|
static transformLiquidSwapQuote(liquidSwapQuote: LiquidSwapQuoteResponse): Quote {
|
|
274
403
|
return {
|
|
275
404
|
amountIn: liquidSwapQuote.execution.details.amountIn,
|
|
@@ -319,7 +448,7 @@ export class QuoteProvider {
|
|
|
319
448
|
tokenIn: params.tokenIn,
|
|
320
449
|
tokenOut: params.tokenOut,
|
|
321
450
|
swapMode: 'ExactIn',
|
|
322
|
-
slippageBps: params.slippageBps ? params.slippageBps :
|
|
451
|
+
slippageBps: params.slippageBps ? params.slippageBps : DEFAULT_SINGLE_CHAIN_SLIPPAGE_BPS,
|
|
323
452
|
});
|
|
324
453
|
}
|
|
325
454
|
|
|
@@ -360,7 +489,7 @@ export class QuoteProvider {
|
|
|
360
489
|
inputMint: params.tokenIn,
|
|
361
490
|
outputMint: params.tokenOut,
|
|
362
491
|
amount: params.amount.toString(),
|
|
363
|
-
slippageBps: params.slippageBps ||
|
|
492
|
+
slippageBps: params.slippageBps || DEFAULT_SINGLE_CHAIN_SLIPPAGE_BPS,
|
|
364
493
|
};
|
|
365
494
|
try {
|
|
366
495
|
return this.transformRaydiumQuote(await raydiumQuoter.getQuote(request));
|
|
@@ -377,7 +506,7 @@ export class QuoteProvider {
|
|
|
377
506
|
inputMint: params.tokenIn,
|
|
378
507
|
outputMint: params.tokenOut,
|
|
379
508
|
amount: params.amount.toString(),
|
|
380
|
-
slippageBps: params.slippageBps ||
|
|
509
|
+
slippageBps: params.slippageBps || DEFAULT_SINGLE_CHAIN_SLIPPAGE_BPS,
|
|
381
510
|
};
|
|
382
511
|
return simpleQuoteToQuote(await pumpfunQuoter.getQuote(request), 'pumpfun');
|
|
383
512
|
}
|
|
@@ -4,7 +4,20 @@ import { ChainID } from '../../../chains.js';
|
|
|
4
4
|
* Solver commission in basis points. MUST equal the solver's COMMISSION env value
|
|
5
5
|
* (solver/src/config.rs). Update both together to avoid quote divergence.
|
|
6
6
|
*/
|
|
7
|
-
export const DEFAULT_COMMISSION_BPS =
|
|
7
|
+
export const DEFAULT_COMMISSION_BPS = 0n; // mirrors solver production COMMISSION=0 (environment/production.yaml)
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The solver's DEFAULT_SLIPPAGE=0.05% (environment/production.yaml) in bps. Applied via
|
|
11
|
+
* get_limit_amount (swap_estimator_rust/src/utils/limit_amount.rs): subtracted from swap
|
|
12
|
+
* outputs (ExactIn) and added to every expense valuation (ExactOut) — see limit-amount.ts.
|
|
13
|
+
*/
|
|
14
|
+
export const SOLVER_DEFAULT_SLIPPAGE_BPS = 5n;
|
|
15
|
+
|
|
16
|
+
/** Default slippage tolerance (bps) for single-chain quotes when the caller does not provide one. */
|
|
17
|
+
export const DEFAULT_SINGLE_CHAIN_SLIPPAGE_BPS = 50;
|
|
18
|
+
|
|
19
|
+
/** Default slippage tolerance (bps) applied to each cross-chain swap leg when the caller does not provide one. */
|
|
20
|
+
export const DEFAULT_CROSS_CHAIN_SLIPPAGE_BPS = 25;
|
|
8
21
|
|
|
9
22
|
/** Single-chain divisor — mirrors evaluate.rs:130-131 (`commission / 10_000`). */
|
|
10
23
|
export const SINGLE_CHAIN_COMMISSION_DENOMINATOR = 10_000n;
|
|
@@ -19,15 +32,46 @@ export const CROSS_CHAIN_COMMISSION_DENOMINATOR = 100_000n;
|
|
|
19
32
|
/** EVM fulfillment gas units — mirrors solver/src/constants/chains.rs (EVM single-chain). */
|
|
20
33
|
export const EVM_SINGLE_CHAIN_GAS_LIMIT = 3_000_000n;
|
|
21
34
|
|
|
22
|
-
/** EVM cross-chain fulfillment gas units — mirrors solver/src/constants/chains.rs (EVM cross-chain). */
|
|
35
|
+
/** EVM cross-chain fulfillment (DEST-leg) gas units — mirrors solver/src/constants/chains.rs (EVM cross-chain). */
|
|
23
36
|
export const EVM_CROSS_CHAIN_GAS_LIMIT = 2_000_000n;
|
|
24
37
|
|
|
25
38
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
39
|
+
* EVM cross-chain SOURCE-leg gas units — mirrors solver chains.rs:
|
|
40
|
+
* start_order_execution_with_swap_cost (1_000_000) + claim_tokens_cost (220_000).
|
|
41
|
+
* The DEST-leg fulfill cost is EVM_CROSS_CHAIN_GAS_LIMIT above.
|
|
42
|
+
*/
|
|
43
|
+
export const EVM_CROSS_CHAIN_SOURCE_GAS_LIMIT = 1_220_000n;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* SOURCE-leg gas units when no source swap is needed (token_in already whitelisted /
|
|
47
|
+
* the stablecoin): start_order_execution_without_swap_cost (400_000) + claim (220_000).
|
|
48
|
+
*/
|
|
49
|
+
export const EVM_CROSS_CHAIN_SOURCE_NO_SWAP_GAS_LIMIT = 620_000n;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Safety margin applied to the execution-price component of gas, on top of the
|
|
53
|
+
* solver-mirror math: the live EVM gas price (the solver evaluates with an eth_gasPrice
|
|
54
|
+
* cached for up to 10 minutes — observed +33% above our live reading on Base) and the
|
|
55
|
+
* Solana transaction/tip costs. Solana rent-exempt account creation (ATA, escrow PDA)
|
|
56
|
+
* is runtime-fixed and NOT padded. Over-provisioning keeps the signed amountOutMin
|
|
57
|
+
* below the solver's max when its gas figure drifts above ours.
|
|
58
|
+
*/
|
|
59
|
+
export const GAS_PRICE_SAFETY_MARGIN_BPS = 2_000n; // +20%
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Fixed native-token gas cost for non-EVM chains without a per-flow formula, in the
|
|
63
|
+
* chain's smallest unit. Sui mirrors the solver's single-chain flat 20_000_000 MIST
|
|
64
|
+
* (solver/src/constants/chains.rs); its cross-chain formula is smaller, so this
|
|
65
|
+
* overstates conservatively. Solana is computed per flow in gas.ts instead.
|
|
29
66
|
*/
|
|
30
67
|
export const NON_EVM_FIXED_NATIVE_GAS_COST: Partial<Record<ChainID, bigint>> = {
|
|
31
68
|
[ChainID.Sui]: 20_000_000n,
|
|
32
|
-
[ChainID.Solana]: 200_000n,
|
|
33
69
|
};
|
|
70
|
+
|
|
71
|
+
/** Solana cost components (lamports) — mirror solver chains.rs and constants/mod.rs (gas_price = 1). */
|
|
72
|
+
export const SOLANA_CREATE_ATA_COST = 2_050_000n;
|
|
73
|
+
export const SOLANA_CREATE_EMPTY_ACC_COST = 890_880n;
|
|
74
|
+
export const SOLANA_JITO_TIP = 500_000n; // solver DEFAULT_JITO_TIP
|
|
75
|
+
export const SOLANA_BASE_TX_COST = 200_000n;
|
|
76
|
+
export const SOLANA_CLAIM_TOKENS_COST = 300_000n;
|
|
77
|
+
export const SOLANA_START_WITHOUT_SWAP_COST = 300_000n; // start_order_execution_without_swap_cost
|
|
@@ -3,11 +3,17 @@ import {
|
|
|
3
3
|
NATIVE_EVM_ETH_ADDRESSES,
|
|
4
4
|
NATIVE_SOLANA_TOKEN_ADDRESS,
|
|
5
5
|
NATIVE_SUI_TOKEN_ADDRESS,
|
|
6
|
+
WRAPPED_ETH_ADDRESSES,
|
|
7
|
+
WRAPPED_SOL_MINT_ADDRESS,
|
|
6
8
|
isNativeEvmToken,
|
|
7
9
|
} from '../../../constants.js';
|
|
8
10
|
import { compareAddresses } from '../address.js';
|
|
9
11
|
import type { Expenses } from './estimate-amount-out.js';
|
|
10
|
-
import { getNativeGasCost } from './gas.js';
|
|
12
|
+
import { getNativeGasCost, type GasFlow } from './gas.js';
|
|
13
|
+
import { exactOutLimitAmount } from './limit-amount.js';
|
|
14
|
+
|
|
15
|
+
/** An extra transfer as the pricing layer sees it — token and amount only, no receiver. */
|
|
16
|
+
export type ExtraTransferAmount = { token: string; amount: bigint };
|
|
11
17
|
|
|
12
18
|
/** Quotes a native->tokenOut swap on the given chain. Injected to keep this module a leaf (no dependency on the aggregator). */
|
|
13
19
|
type GasQuoteFn = (params: {
|
|
@@ -25,38 +31,88 @@ function nativeTokenAddressForChain(chainId: SupportedChain): string {
|
|
|
25
31
|
return NATIVE_EVM_ETH_ADDRESSES[0]!;
|
|
26
32
|
}
|
|
27
33
|
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
/**
|
|
35
|
+
* True when 1 native smallest-unit equals 1 unit of `address`: the native token itself
|
|
36
|
+
* or its canonical wrapped form (wrapping is 1:1, and routers have no "route" for it,
|
|
37
|
+
* so quoting native->wrapped would fail despite being trivially priceable).
|
|
38
|
+
*/
|
|
39
|
+
function isNativeOrWrappedNative(chainId: SupportedChain, address: string): boolean {
|
|
40
|
+
if (chainId === ChainID.Solana) {
|
|
41
|
+
return (
|
|
42
|
+
compareAddresses(address, NATIVE_SOLANA_TOKEN_ADDRESS) || compareAddresses(address, WRAPPED_SOL_MINT_ADDRESS)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
30
45
|
if (chainId === ChainID.Sui) return compareAddresses(address, NATIVE_SUI_TOKEN_ADDRESS);
|
|
31
|
-
return isNativeEvmToken(address);
|
|
46
|
+
return isNativeEvmToken(address) || compareAddresses(address, WRAPPED_ETH_ADDRESSES[chainId]);
|
|
32
47
|
}
|
|
33
48
|
|
|
34
49
|
/**
|
|
35
50
|
* Builds the Expenses bundle in the output token. Gas is fetched in native units and
|
|
36
51
|
* swapped into tokenOut via the injected quote fn, mirroring how the solver converts
|
|
37
|
-
* expenses into the output token.
|
|
52
|
+
* expenses into the output token. Extra transfers (e.g. relayer/affiliate payouts) are
|
|
53
|
+
* taken 1:1 when already denominated in tokenOut, or quoted otherwise.
|
|
54
|
+
*
|
|
55
|
+
* Padding mirrors the flow's solver code path:
|
|
56
|
+
* - single-chain applies get_limit_amount(ExactOut) uniformly to every expense conversion
|
|
57
|
+
* (evaluate.rs:116-128), even same-token / native-wrapped short-circuits — pad everything;
|
|
58
|
+
* - cross-chain values expenses via estimate_tokens_value_in_stablecoins
|
|
59
|
+
* (cross_chain_estimation.rs:452-475), which adds stablecoin-denominated amounts directly
|
|
60
|
+
* and pads only USD-converted ones — leave amounts already in tokenOut unpadded.
|
|
38
61
|
*/
|
|
39
62
|
export async function estimateExpensesInTokenOut(params: {
|
|
40
63
|
chainId: SupportedChain;
|
|
41
64
|
tokenOut: string;
|
|
42
|
-
gasLimit: bigint;
|
|
43
65
|
getQuote: GasQuoteFn;
|
|
66
|
+
extraTransfers?: ExtraTransferAmount[];
|
|
67
|
+
protocolFeeInTokenOut?: bigint;
|
|
68
|
+
flow?: GasFlow;
|
|
44
69
|
rpcProviderUrl?: string;
|
|
45
70
|
}): Promise<Expenses> {
|
|
46
|
-
const { chainId, tokenOut,
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
71
|
+
const { chainId, tokenOut, getQuote, extraTransfers, protocolFeeInTokenOut, flow, rpcProviderUrl } = params;
|
|
72
|
+
const padDirectAmounts = (flow ?? 'singleChain') === 'singleChain';
|
|
73
|
+
const padDirect = (amount: bigint) => (padDirectAmounts ? exactOutLimitAmount(amount) : amount);
|
|
74
|
+
|
|
75
|
+
// Value each extra transfer in tokenOut concurrently with the gas legs — they share no inputs.
|
|
76
|
+
// Those already in tokenOut skip the quote.
|
|
77
|
+
const extraTransfersPromise = Promise.all(
|
|
78
|
+
(extraTransfers ?? []).map((transfer) =>
|
|
79
|
+
compareAddresses(transfer.token, tokenOut)
|
|
80
|
+
? Promise.resolve(padDirect(transfer.amount))
|
|
81
|
+
: getQuote({ chainId, amount: transfer.amount, tokenIn: transfer.token, tokenOut }).then((q) =>
|
|
82
|
+
exactOutLimitAmount(q.amountOut),
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
// Register a handler so a transfer-quote rejection is not reported as unhandled
|
|
87
|
+
// if the gas leg throws first; the await below still surfaces the error.
|
|
88
|
+
extraTransfersPromise.catch(() => {});
|
|
89
|
+
|
|
90
|
+
// Worst-case receiver count: destination address plus one distinct receiver per extra
|
|
91
|
+
// transfer — mirrors the solver's max_cost_of_accounts_creation (Solana ATA per receiver).
|
|
92
|
+
const uniqueReceivers = 1 + (extraTransfers?.length ?? 0);
|
|
93
|
+
const nativeGasCost = await getNativeGasCost({ chainId, flow, uniqueReceivers, rpcProviderUrl });
|
|
94
|
+
|
|
95
|
+
// A zero-amount swap always yields zero out, so skip the quote call entirely
|
|
96
|
+
// rather than round-tripping to the quoting service for nothing.
|
|
97
|
+
const gasInTokenOut = exactOutLimitAmount(
|
|
98
|
+
isNativeOrWrappedNative(chainId, tokenOut) || nativeGasCost === 0n
|
|
99
|
+
? nativeGasCost
|
|
100
|
+
: (
|
|
101
|
+
await getQuote({
|
|
102
|
+
chainId,
|
|
103
|
+
amount: nativeGasCost,
|
|
104
|
+
tokenIn: nativeTokenAddressForChain(chainId),
|
|
105
|
+
tokenOut,
|
|
106
|
+
})
|
|
107
|
+
).amountOut,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const extraTransferAmounts = await extraTransfersPromise;
|
|
111
|
+
const extraTransfersInTokenOut = extraTransferAmounts.reduce((sum, amount) => sum + amount, 0n);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
gasInTokenOut,
|
|
115
|
+
protocolFeeInTokenOut: padDirect(protocolFeeInTokenOut ?? 0n),
|
|
116
|
+
extraTransfersInTokenOut,
|
|
117
|
+
};
|
|
62
118
|
}
|