@shogun-sdk/intents-sdk 1.4.1 → 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.
@@ -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
- function isNativeToken(chainId: SupportedChain, address: string): boolean {
29
- if (chainId === ChainID.Solana) return compareAddresses(address, NATIVE_SOLANA_TOKEN_ADDRESS);
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. protocolFee/extraTransfers are reserved (0n) for now.
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, gasLimit, getQuote, rpcProviderUrl } = params;
47
-
48
- const nativeGasCost = await getNativeGasCost({ chainId, gasLimit, rpcProviderUrl });
49
-
50
- const gasInTokenOut = isNativeToken(chainId, tokenOut)
51
- ? nativeGasCost
52
- : (
53
- await getQuote({
54
- chainId,
55
- amount: nativeGasCost,
56
- tokenIn: nativeTokenAddressForChain(chainId),
57
- tokenOut,
58
- })
59
- ).amountOut;
60
-
61
- return { gasInTokenOut, protocolFeeInTokenOut: 0n, extraTransfersInTokenOut: 0n };
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
  }
@@ -1,23 +1,88 @@
1
1
  import { getGasPrice } from 'viem/actions';
2
- import { isEvmChain, type SupportedChain } from '../../../chains.js';
2
+ import { ChainID, isEvmChain, type SupportedChain } from '../../../chains.js';
3
3
  import { ChainProvider } from '../../../core/evm/chain-provider.js';
4
- import { NON_EVM_FIXED_NATIVE_GAS_COST } from './constants.js';
4
+ import {
5
+ EVM_CROSS_CHAIN_GAS_LIMIT,
6
+ EVM_CROSS_CHAIN_SOURCE_GAS_LIMIT,
7
+ EVM_CROSS_CHAIN_SOURCE_NO_SWAP_GAS_LIMIT,
8
+ EVM_SINGLE_CHAIN_GAS_LIMIT,
9
+ GAS_PRICE_SAFETY_MARGIN_BPS,
10
+ NON_EVM_FIXED_NATIVE_GAS_COST,
11
+ SOLANA_BASE_TX_COST,
12
+ SOLANA_CLAIM_TOKENS_COST,
13
+ SOLANA_CREATE_ATA_COST,
14
+ SOLANA_CREATE_EMPTY_ACC_COST,
15
+ SOLANA_JITO_TIP,
16
+ SOLANA_START_WITHOUT_SWAP_COST,
17
+ } from './constants.js';
18
+
19
+ /**
20
+ * Which solver execution the gas estimate models. crossChainSourceNoSwap is the source
21
+ * leg when token_in needs no swap into the stablecoin (start_order_execution_without_swap).
22
+ */
23
+ export type GasFlow = 'singleChain' | 'crossChainSource' | 'crossChainSourceNoSwap' | 'crossChainDest';
24
+
25
+ /** EVM gas units per execution flow — pairs each flow with its solver gas limit in one place. */
26
+ const EVM_GAS_LIMIT_BY_FLOW: Record<GasFlow, bigint> = {
27
+ singleChain: EVM_SINGLE_CHAIN_GAS_LIMIT,
28
+ crossChainSource: EVM_CROSS_CHAIN_SOURCE_GAS_LIMIT,
29
+ crossChainSourceNoSwap: EVM_CROSS_CHAIN_SOURCE_NO_SWAP_GAS_LIMIT,
30
+ crossChainDest: EVM_CROSS_CHAIN_GAS_LIMIT,
31
+ };
32
+
33
+ /** Pads the execution-price component of gas (fees/tips), never rent-exempt deposits. */
34
+ function padGasPrice(amount: bigint): bigint {
35
+ return (amount * (10_000n + GAS_PRICE_SAFETY_MARGIN_BPS)) / 10_000n;
36
+ }
37
+
38
+ /**
39
+ * Mirrors solver chains.rs Solana branches with gas_price = 1 (the solver's unwrap_or default).
40
+ * ATA creation scales with payout receivers; cross-chain adds the escrow PDA / claim legs.
41
+ * Only the transaction/tip component takes the safety margin — rent-exempt account
42
+ * creation (ATA, escrow PDA) is runtime-fixed and cannot drift.
43
+ */
44
+ function solanaNativeGasCost(flow: GasFlow, uniqueReceivers: number): bigint {
45
+ switch (flow) {
46
+ case 'crossChainSource':
47
+ // start_order_execution_with_swap_cost + claim_tokens_cost (receivers do not apply)
48
+ return padGasPrice(SOLANA_BASE_TX_COST + SOLANA_JITO_TIP + SOLANA_CLAIM_TOKENS_COST);
49
+ case 'crossChainSourceNoSwap':
50
+ // start_order_execution_without_swap_cost + claim_tokens_cost
51
+ return padGasPrice(SOLANA_START_WITHOUT_SWAP_COST + SOLANA_CLAIM_TOKENS_COST);
52
+ case 'crossChainDest':
53
+ // fulfill_cross_chain_order_cost
54
+ return (
55
+ SOLANA_CREATE_EMPTY_ACC_COST +
56
+ SOLANA_CREATE_ATA_COST * BigInt(uniqueReceivers) +
57
+ padGasPrice(SOLANA_BASE_TX_COST + SOLANA_JITO_TIP)
58
+ );
59
+ case 'singleChain':
60
+ // fulfill_single_chain_order_cost
61
+ return SOLANA_CREATE_ATA_COST * BigInt(uniqueReceivers) + padGasPrice(SOLANA_BASE_TX_COST + SOLANA_JITO_TIP);
62
+ }
63
+ }
5
64
 
6
65
  /**
7
66
  * Gas cost in the chain's native smallest unit (wei / lamports / MIST).
8
- * EVM: live gas price × gas limit. Non-EVM: solver-mirrored fixed cost.
67
+ * EVM: live gas price × the flow's gas limit. Solana: solver-mirrored per-flow formula.
68
+ * Other non-EVM: solver-mirrored fixed cost.
9
69
  */
10
70
  export async function getNativeGasCost(params: {
11
71
  chainId: SupportedChain;
12
- gasLimit: bigint;
72
+ flow?: GasFlow;
73
+ uniqueReceivers?: number;
13
74
  rpcProviderUrl?: string;
14
75
  }): Promise<bigint> {
15
- const { chainId, gasLimit, rpcProviderUrl } = params;
76
+ const { chainId, flow = 'singleChain', uniqueReceivers = 1, rpcProviderUrl } = params;
16
77
 
17
78
  if (isEvmChain(chainId)) {
18
79
  const client = ChainProvider.getClient(chainId, rpcProviderUrl);
19
80
  const gasPrice = await getGasPrice(client);
20
- return gasLimit * gasPrice;
81
+ return padGasPrice(EVM_GAS_LIMIT_BY_FLOW[flow] * gasPrice);
82
+ }
83
+
84
+ if (chainId === ChainID.Solana) {
85
+ return solanaNativeGasCost(flow, uniqueReceivers);
21
86
  }
22
87
 
23
88
  const fixed = NON_EVM_FIXED_NATIVE_GAS_COST[chainId];
@@ -0,0 +1,21 @@
1
+ import { SOLVER_DEFAULT_SLIPPAGE_BPS } from './constants.js';
2
+
3
+ /**
4
+ * Mirrors the solver's get_limit_amount (swap_estimator_rust/src/utils/limit_amount.rs)
5
+ * with Slippage::Percent(DEFAULT_SLIPPAGE): the diff is `mul_div(amount, slippage, base,
6
+ * round_up=true)` — always rounded UP — then subtracted for ExactIn (swap outputs are
7
+ * worst-cased down) or added for ExactOut (expense valuations are worst-cased up).
8
+ */
9
+ function slippageDiff(amountQuote: bigint): bigint {
10
+ return (amountQuote * SOLVER_DEFAULT_SLIPPAGE_BPS + 9_999n) / 10_000n;
11
+ }
12
+
13
+ /** Swap output the solver actually credits: `amount_limit`, not the nominal quote. */
14
+ export function exactInLimitAmount(amountQuote: bigint): bigint {
15
+ return amountQuote - slippageDiff(amountQuote);
16
+ }
17
+
18
+ /** Expense valuation the solver actually charges: quote plus the rounded-up slippage diff. */
19
+ export function exactOutLimitAmount(amountQuote: bigint): bigint {
20
+ return amountQuote + slippageDiff(amountQuote);
21
+ }
@@ -10,6 +10,7 @@ import {
10
10
  type LaunchpadPoolInfo,
11
11
  } from '@raydium-io/raydium-sdk-v2';
12
12
  import { type SimpleQuote } from './aggregator.js';
13
+ import { compareAddresses } from './address.js';
13
14
  import { Connection, clusterApiUrl, PublicKey } from '@solana/web3.js'; // Used this package for raydium-sdk-v2 compatibility
14
15
  import BN from 'bn.js';
15
16
  import { Decimal } from 'decimal.js';
@@ -51,10 +52,6 @@ const platformConfigCache = new Map<
51
52
 
52
53
  const mintInfoCache = new Map<string, Awaited<ReturnType<Raydium['token']['getTokenInfo']>>>();
53
54
 
54
- // Local implementation of compareAddresses function
55
- const compareAddresses = (firstAddress?: string, secondAddress?: string): boolean => {
56
- return !!firstAddress && !!secondAddress && firstAddress.toLowerCase() === secondAddress.toLowerCase();
57
- };
58
55
 
59
56
  const LAUNCHPAD_WHITELISTED_QUOTE_TOKENS = [
60
57
  new PublicKey('So11111111111111111111111111111111111111112'), // WSOL