@shogun-sdk/intents-sdk 1.2.6-test.14 → 1.2.6-test.16

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.
@@ -1,6 +1,6 @@
1
1
  import type { SupportedChain } from '../chains.js';
2
2
  import type { CrossChainOrder } from '../core/orders/cross-chain.js';
3
- import type { ExtraTransfer } from '../core/orders/common.js';
3
+ import type { ExtraTransfer, StopLossType } from '../core/orders/common.js';
4
4
  import type { SingleChainOrder } from '../core/orders/single-chain.js';
5
5
  import type { DcaSingleChainOrder } from '../core/orders/dca-single-chain.js';
6
6
 
@@ -20,7 +20,11 @@ export type ExecutionDetails = {
20
20
  /** Extra transfers to be made */
21
21
  extraTransfers?: ExtraTransfer[];
22
22
 
23
- stopLossMaxOut?: bigint;
23
+ /** Stop loss type, must match the auctioneer's `stopLossType` field */
24
+ stopLossType?: StopLossType;
25
+ /** Initial trigger price (tokenIn/tokenOut) that arms the stop loss */
26
+ stopLossTriggerPrice?: string;
27
+ /** Take profit minimum amount out */
24
28
  takeProfitMinOut?: bigint;
25
29
  };
26
30
 
@@ -96,6 +100,36 @@ export type SingleChainUserIntentRequest = {
96
100
  };
97
101
  };
98
102
 
103
+ /**
104
+ * Source chain data for a cross-chain DCA order. Mirrors the auctioneer's
105
+ * `CrossChainDcaOrderGenericRequestData` (common cross-chain fields + flattened
106
+ * `CommonDcaOrderData`). The destination fields live in `executionDetails`.
107
+ */
108
+ export type DcaCrossChainSourceChainData = {
109
+ user: string;
110
+ srcChainId: SupportedChain;
111
+ tokenIn: string;
112
+ minStablecoinsAmount: bigint;
113
+ deadline: number;
114
+ executionDetailsHash: ExecutionDetailsHash;
115
+ /** Flattened CommonDcaOrderData */
116
+ startTime: number;
117
+ amountInPerInterval: bigint;
118
+ totalIntervals: number;
119
+ intervalDuration: number;
120
+ };
121
+
122
+ export type DcaCrossChainUserIntentRequest = {
123
+ genericData: DcaCrossChainSourceChainData;
124
+ /** JSON-stringified execution details for the destination chain */
125
+ executionDetails: string;
126
+ chainSpecificData: {
127
+ EVM?: DcaSingleChainEVMPreparedData;
128
+ Solana?: DcaSingleChainSolanaPreparedData;
129
+ Sui?: DcaSingleChainSuiPreparedData;
130
+ };
131
+ };
132
+
99
133
  export type ChainPreparedData = EvmPreparedData | SuiPreparedData | SolanaPreparedData;
100
134
 
101
135
  export type CrossChainOrderPrepared = {
@@ -32,10 +32,6 @@ export abstract class BaseValidator implements IChainValidator {
32
32
  this.validateAmount(order.amountIn);
33
33
  this.validateAmount(order.amountOutMin ?? 1n);
34
34
 
35
- if (order.stopLossMaxOut) {
36
- this.validateAmount(order.stopLossMaxOut);
37
- }
38
-
39
35
  if (order.takeProfitMinOut) {
40
36
  this.validateAmount(order.takeProfitMinOut);
41
37
  }
@@ -1,36 +1,13 @@
1
1
  import { createHash } from 'crypto';
2
- import type { ExtraTransfer } from '../core/orders/common.js';
3
- import { Parsers } from './parsers.js';
4
-
5
- export function generateExecutionDetailsHash({
6
- destChainId,
7
- destinationAddress,
8
- tokenOut,
9
- amountOutMin,
10
- stopLossMaxOut,
11
- takeProfitMinOut,
12
- extraTransfers,
13
- }: {
14
- destChainId: number;
15
- destinationAddress: string;
16
- tokenOut: string;
17
- amountOutMin: string;
18
- stopLossMaxOut?: string;
19
- takeProfitMinOut?: string;
20
- extraTransfers?: ExtraTransfer[];
21
- }): string {
22
- const executionDetails = JSON.stringify(
23
- {
24
- destChainId,
25
- destinationAddress,
26
- tokenOut,
27
- amountOutMin,
28
- stopLossMaxOut,
29
- takeProfitMinOut,
30
- extraTransfers,
31
- },
32
- Parsers.bigIntReplacer,
33
- );
34
2
 
3
+ /**
4
+ * Computes the SHA-256 hash the auctioneer expects for cross-chain execution details.
5
+ *
6
+ * The auctioneer recomputes sha256 over the exact `executionDetails` JSON string it
7
+ * receives and rejects the order when it differs (hash mismatch). Callers therefore MUST
8
+ * hash the same serialized string they send on the wire — pass that string here directly
9
+ * instead of re-serializing a parallel object, which is how key-order/field drift creeps in.
10
+ */
11
+ export function generateExecutionDetailsHash(executionDetails: string): string {
35
12
  return '0x' + createHash('sha256').update(executionDetails).digest('hex');
36
13
  }
@@ -0,0 +1,4 @@
1
+ /** Case-insensitive address equality; false if either address is missing. */
2
+ export function compareAddresses(firstAddress?: string, secondAddress?: string): boolean {
3
+ return !!firstAddress && !!secondAddress && firstAddress.toLowerCase() === secondAddress.toLowerCase();
4
+ }
@@ -9,10 +9,11 @@ import { LiquidSwapQuoteProvider, type LiquidSwapQuoteResponse } from './liquids
9
9
  import { RaydiumQuoteProvider, type RaydiumPumpQuoteParams, type RaydiumQuoteResponse } from './raydium.js';
10
10
  import { PumpFunQuoteProvider } from './pumpfun/index.js';
11
11
  import { RelayQuoteProvider } from './relay.js';
12
-
13
- const compareAddresses = (firstAddress?: string, secondAddress?: string): boolean => {
14
- return !!firstAddress && !!secondAddress && firstAddress.toLowerCase() === secondAddress.toLowerCase();
15
- };
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 { DEFAULT_COMMISSION_BPS, CROSS_CHAIN_COMMISSION_DENOMINATOR, EVM_CROSS_CHAIN_GAS_LIMIT } from './pricing/constants.js';
16
+ import { compareAddresses } from './address.js';
16
17
 
17
18
  type SingleChainQuoteParams = {
18
19
  chainId: ChainID;
@@ -95,6 +96,7 @@ export class QuoteProvider {
95
96
  },
96
97
  };
97
98
  } catch (e) {
99
+ if (e instanceof IntentNotViableError) throw e;
98
100
  console.error('Error getting quote from routers:', e);
99
101
  throw new Error('Failed to get quote from routers');
100
102
  }
@@ -136,12 +138,22 @@ export class QuoteProvider {
136
138
  tokenOut: sourceStable.address,
137
139
  };
138
140
 
139
- const sourceQuote = await this.getSingleChainQuote(sourceSingleChainQuoteParams);
141
+ const [sourceQuote, expenses] = await Promise.all([
142
+ this.getSingleChainQuote(sourceSingleChainQuoteParams),
143
+ estimateExpensesInTokenOut({
144
+ chainId: params.destChainId,
145
+ tokenOut: params.tokenOut,
146
+ gasLimit: EVM_CROSS_CHAIN_GAS_LIMIT,
147
+ getQuote: (p) => this.getSingleChainQuote(p),
148
+ }),
149
+ ]);
140
150
 
141
151
  // Step 2: Normalize stable amount between source and destination ---
142
152
  // Adjust for decimal difference between stables (e.g. 6 → 18)
143
- const normalizedBridgeAmount = BigInt(
144
- Math.floor((Number(sourceQuote.amountOut) / 10 ** sourceStable.decimals) * 10 ** destStable.decimals),
153
+ const normalizedBridgeAmount = convertDecimals(
154
+ sourceQuote.amountOut,
155
+ sourceStable.decimals,
156
+ destStable.decimals,
145
157
  );
146
158
 
147
159
  // Step 3: Get quote from dest bridge stable → target token ---
@@ -154,17 +166,25 @@ export class QuoteProvider {
154
166
 
155
167
  const destQuote = await this.getSingleChainQuote(destSingleChainQuoteParams);
156
168
 
157
- // -Step 4: Compute stable min-amount with correct decimals ---
169
+ // Step 4: Compute stable min-amount with correct decimals ---
158
170
  const estimatedAmountInAsMinStablecoinAmount = BigInt(
159
171
  Math.floor(sourceQuote.amountInUsd * 10 ** sourceStable.decimals),
160
172
  );
161
173
 
174
+ const swapOutput = destQuote.amountOutMin ?? destQuote.amountOut;
175
+ const estimatedAmountOutReduced = estimateAmountOut(
176
+ swapOutput,
177
+ DEFAULT_COMMISSION_BPS,
178
+ expenses,
179
+ CROSS_CHAIN_COMMISSION_DENOMINATOR,
180
+ );
181
+
162
182
  return {
163
183
  estimatedAmountOut: destQuote.amountOut,
164
184
  estimatedAmountOutUsd: destQuote.amountOutUsd,
165
185
  amountInUsd: sourceQuote.amountInUsd,
166
186
  estimatedAmountInAsMinStablecoinAmount,
167
- estimatedAmountOutReduced: destQuote.amountOutMin || destQuote.amountOut,
187
+ estimatedAmountOutReduced,
168
188
  estimatedAmountOutUsdReduced: destQuote.amountOutUsd,
169
189
  };
170
190
  }
@@ -0,0 +1,33 @@
1
+ import { ChainID } from '../../../chains.js';
2
+
3
+ /**
4
+ * Solver commission in basis points. MUST equal the solver's COMMISSION env value
5
+ * (solver/src/config.rs). Update both together to avoid quote divergence.
6
+ */
7
+ export const DEFAULT_COMMISSION_BPS = 50n; // 0.5% — placeholder, confirm against solver COMMISSION
8
+
9
+ /** Single-chain divisor — mirrors evaluate.rs:130-131 (`commission / 10_000`). */
10
+ export const SINGLE_CHAIN_COMMISSION_DENOMINATOR = 10_000n;
11
+
12
+ /**
13
+ * Cross-chain divisor — mirrors cross_chain_estimation.rs:213-214 (`commission / 100_000`).
14
+ * The asymmetry with the single-chain divisor is intentional (verbatim solver mirror);
15
+ * confirm with the solver team whether this is a unit choice or a bug before relying on it.
16
+ */
17
+ export const CROSS_CHAIN_COMMISSION_DENOMINATOR = 100_000n;
18
+
19
+ /** EVM fulfillment gas units — mirrors solver/src/constants/chains.rs (EVM single-chain). */
20
+ export const EVM_SINGLE_CHAIN_GAS_LIMIT = 3_000_000n;
21
+
22
+ /** EVM cross-chain fulfillment gas units — mirrors solver/src/constants/chains.rs (EVM cross-chain). */
23
+ export const EVM_CROSS_CHAIN_GAS_LIMIT = 2_000_000n;
24
+
25
+ /**
26
+ * Fixed native-token gas cost for non-EVM chains, in the chain's smallest unit.
27
+ * Mirrors solver/src/constants/chains.rs: Sui = 20_000_000 MIST; Solana base = 200_000 lamports
28
+ * (single-receiver default; per-receiver ATA cost is not modeled at quote time).
29
+ */
30
+ export const NON_EVM_FIXED_NATIVE_GAS_COST: Partial<Record<ChainID, bigint>> = {
31
+ [ChainID.Sui]: 20_000_000n,
32
+ [ChainID.Solana]: 200_000n,
33
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Integer decimal conversion between two tokens, mirroring solver cross_chain_estimation.rs:205-211.
3
+ * Uses BigInt powers of ten to avoid the float precision loss of `Math.floor(Number(...))`.
4
+ */
5
+ export function convertDecimals(amount: bigint, fromDecimals: number, toDecimals: number): bigint {
6
+ const diff = toDecimals - fromDecimals;
7
+ if (diff >= 0) {
8
+ return amount * 10n ** BigInt(diff);
9
+ }
10
+ return amount / 10n ** BigInt(-diff);
11
+ }
@@ -0,0 +1,36 @@
1
+ /** Expense components, all expressed in the output token's smallest units. */
2
+ export type Expenses = {
3
+ gasInTokenOut: bigint;
4
+ protocolFeeInTokenOut: bigint;
5
+ extraTransfersInTokenOut: bigint;
6
+ };
7
+
8
+ /** Thrown when total expenses meet or exceed the swap output, mirroring the solver's rejection. */
9
+ export class IntentNotViableError extends Error {
10
+ constructor(message: string) {
11
+ super(message);
12
+ this.name = 'IntentNotViableError';
13
+ }
14
+ }
15
+
16
+ /**
17
+ * Ports solver_rust evaluate.rs: final amount = swapOutput − expenses − commission.
18
+ * `commissionBps` is basis points; `commissionDenominator` selects single-chain (10_000n)
19
+ * vs cross-chain (100_000n) semantics. Throws IntentNotViableError when nothing is left.
20
+ */
21
+ export function estimateAmountOut(
22
+ swapOutput: bigint,
23
+ commissionBps: bigint,
24
+ expenses: Expenses,
25
+ commissionDenominator = 10_000n,
26
+ ): bigint {
27
+ const commissionFee = (swapOutput * commissionBps) / commissionDenominator;
28
+ const total =
29
+ expenses.gasInTokenOut + expenses.protocolFeeInTokenOut + expenses.extraTransfersInTokenOut + commissionFee;
30
+
31
+ if (total >= swapOutput) {
32
+ throw new IntentNotViableError(`Total expenses (${total}) meet or exceed swap output (${swapOutput})`);
33
+ }
34
+
35
+ return swapOutput - total;
36
+ }
@@ -0,0 +1,62 @@
1
+ import { ChainID, type SupportedChain } from '../../../chains.js';
2
+ import {
3
+ NATIVE_EVM_ETH_ADDRESSES,
4
+ NATIVE_SOLANA_TOKEN_ADDRESS,
5
+ NATIVE_SUI_TOKEN_ADDRESS,
6
+ isNativeEvmToken,
7
+ } from '../../../constants.js';
8
+ import { compareAddresses } from '../address.js';
9
+ import type { Expenses } from './estimate-amount-out.js';
10
+ import { getNativeGasCost } from './gas.js';
11
+
12
+ /** Quotes a native->tokenOut swap on the given chain. Injected to keep this module a leaf (no dependency on the aggregator). */
13
+ type GasQuoteFn = (params: {
14
+ chainId: SupportedChain;
15
+ amount: bigint;
16
+ tokenIn: string;
17
+ tokenOut: string;
18
+ }) => Promise<{ amountOut: bigint }>;
19
+
20
+ function nativeTokenAddressForChain(chainId: SupportedChain): string {
21
+ if (chainId === ChainID.Solana) return NATIVE_SOLANA_TOKEN_ADDRESS;
22
+ if (chainId === ChainID.Sui) return NATIVE_SUI_TOKEN_ADDRESS;
23
+ // EVM native sentinel addresses share the same 0xEee... value; use the first.
24
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
25
+ return NATIVE_EVM_ETH_ADDRESSES[0]!;
26
+ }
27
+
28
+ function isNativeToken(chainId: SupportedChain, address: string): boolean {
29
+ if (chainId === ChainID.Solana) return compareAddresses(address, NATIVE_SOLANA_TOKEN_ADDRESS);
30
+ if (chainId === ChainID.Sui) return compareAddresses(address, NATIVE_SUI_TOKEN_ADDRESS);
31
+ return isNativeEvmToken(address);
32
+ }
33
+
34
+ /**
35
+ * Builds the Expenses bundle in the output token. Gas is fetched in native units and
36
+ * 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.
38
+ */
39
+ export async function estimateExpensesInTokenOut(params: {
40
+ chainId: SupportedChain;
41
+ tokenOut: string;
42
+ gasLimit: bigint;
43
+ getQuote: GasQuoteFn;
44
+ rpcProviderUrl?: string;
45
+ }): 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 };
62
+ }
@@ -0,0 +1,28 @@
1
+ import { getGasPrice } from 'viem/actions';
2
+ import { isEvmChain, type SupportedChain } from '../../../chains.js';
3
+ import { ChainProvider } from '../../../core/evm/chain-provider.js';
4
+ import { NON_EVM_FIXED_NATIVE_GAS_COST } from './constants.js';
5
+
6
+ /**
7
+ * 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.
9
+ */
10
+ export async function getNativeGasCost(params: {
11
+ chainId: SupportedChain;
12
+ gasLimit: bigint;
13
+ rpcProviderUrl?: string;
14
+ }): Promise<bigint> {
15
+ const { chainId, gasLimit, rpcProviderUrl } = params;
16
+
17
+ if (isEvmChain(chainId)) {
18
+ const client = ChainProvider.getClient(chainId, rpcProviderUrl);
19
+ const gasPrice = await getGasPrice(client);
20
+ return gasLimit * gasPrice;
21
+ }
22
+
23
+ const fixed = NON_EVM_FIXED_NATIVE_GAS_COST[chainId];
24
+ if (fixed === undefined) {
25
+ throw new Error(`No fixed native gas cost configured for chain ${chainId}`);
26
+ }
27
+ return fixed;
28
+ }