@robertprp/intents-sdk
Version:
Shogun Network Intent-based cross-chain swaps SDK
175 lines (147 loc) • 5.82 kB
text/typescript
import { ChainID, chainIdToChainTypeMap, isEvmChain, type SupportedChain } from '../../chains.js';
import { ValidationError } from '../../errors/index.js';
import type { ApiResponse } from '../../types/api.js';
import type { SingleChainPreparedData, SingleChainUserIntentRequest } from '../../types/intent.js';
import { SingleChainOrderValidator } from '../../utils/order-validator.js';
import { QuoteProvider } from '../../utils/quote/aggregator.js';
import { getEVMSingleChainOrderTypedData } from '../evm/order-signature.js';
import { BaseSDK } from '../sdk.js';
import { getSolanaSingleChainOrderInstructions } from '../solana/order-instructions.js';
import { type ExtraTransfer } from './common.js';
export type CreateSingleChainOrderParams = {
chainId: SupportedChain;
amountIn: bigint;
tokenIn: string;
tokenOut: string;
amountOutMin?: bigint;
destinationAddress: string;
extraTransfers?: ExtraTransfer[];
deadline: number;
stopLossMaxOut?: bigint;
takeProfitMinOut?: bigint;
};
type OrderScenario = 'QUOTE_REQUIRED' | 'USE_PROVIDED_AMOUNT' | 'STOP_LOSS_ONLY' | 'BOTH_PROVIDED';
export class SingleChainOrder {
public user: string;
public chainId: SupportedChain;
public amountIn: bigint;
public tokenIn: string;
public tokenOut: string;
public amountOutMin: bigint;
public destinationAddress: string;
public extraTransfers?: ExtraTransfer[];
public deadline: number;
public stopLossMaxOut?: bigint;
public takeProfitMinOut?: bigint;
private constructor(params: CreateSingleChainOrderParams & { user: string }) {
this.user = params.user;
this.chainId = params.chainId;
this.amountIn = params.amountIn;
this.tokenIn = params.tokenIn;
this.tokenOut = params.tokenOut;
this.amountOutMin = params.amountOutMin ?? 1n;
this.destinationAddress = params.destinationAddress;
this.extraTransfers = params.extraTransfers;
this.deadline = params.deadline;
this.stopLossMaxOut = params.stopLossMaxOut;
this.takeProfitMinOut = params.takeProfitMinOut;
}
public static async create(input: CreateSingleChainOrderParams & { user: string }): Promise<SingleChainOrder> {
new SingleChainOrderValidator().validateOrder(input);
const amountOutMin = await this.calculateAmountOutMin(input);
const order = new SingleChainOrder({
...input,
amountOutMin,
user: input.user,
});
const preparedRandomData = order.getRandomPreparedData();
const intentRequest = order.toIntentRequest(preparedRandomData);
await BaseSDK.validateSingleChainOrder(intentRequest);
return order;
}
/// This is needed because API requires the prepared data to be send
/// In the cases of Solana and Sui, if we want the real data, we must send the order on-chain before validating.
/// And that is something we cannot do before validating the order on the API side.
private getRandomPreparedData(): SingleChainPreparedData {
const chainId = this.chainId;
const chainType = chainIdToChainTypeMap[chainId];
const randomNumber = String(Math.floor(Math.random() * 10000000));
switch (chainType) {
case 'EVM': {
return { nonce: randomNumber, signature: '0x0000000000000000000000000000000000000000000000000000000000000000' };
}
case 'Solana': {
return { secretNumber: randomNumber, orderPubkey: 'DFNAjFAvS4GF98Tp1kiyLvEHM3wjGXibCfF86nnmhuVc' };
}
case 'Sui': {
const digest = 'FQWndwYJhNQUoHyvR8UuhGURC2EKx9eWErFm9Tc2DggF';
return { transactionHash: digest };
}
default: {
throw new Error('Chain type not supported');
}
}
}
private static async calculateAmountOutMin(input: CreateSingleChainOrderParams): Promise<bigint> {
const { amountOutMin, stopLossMaxOut } = input;
const scenario = this.getSingleChainOrderScenario({
hasAmountOutMin: !!amountOutMin,
hasStopLoss: !!stopLossMaxOut,
});
switch (scenario) {
case 'QUOTE_REQUIRED':
const quote = await QuoteProvider.getSingleChainQuote({
tokenIn: input.tokenIn,
amount: input.amountIn,
chainId: input.chainId,
tokenOut: input.tokenOut,
});
return (quote.amountOut * 93n) / 100n; // Add 7% reduced to cover fees
case 'USE_PROVIDED_AMOUNT':
return amountOutMin!;
case 'STOP_LOSS_ONLY':
case 'BOTH_PROVIDED':
// When stop loss is involved, amountOutMin should be 1
return 1n;
}
}
private static getSingleChainOrderScenario({
hasAmountOutMin,
hasStopLoss,
}: {
hasAmountOutMin: boolean;
hasStopLoss: boolean;
}): OrderScenario {
if (!hasAmountOutMin && !hasStopLoss) return 'QUOTE_REQUIRED';
if (hasAmountOutMin && !hasStopLoss) return 'USE_PROVIDED_AMOUNT';
if (!hasAmountOutMin && hasStopLoss) return 'STOP_LOSS_ONLY';
return 'BOTH_PROVIDED';
}
public toIntentRequest(preparedData: SingleChainPreparedData): SingleChainUserIntentRequest {
const sourceChainType = chainIdToChainTypeMap[this.chainId];
return {
genericData: this,
chainSpecificData: {
[sourceChainType]: preparedData,
},
};
}
public sendToAuctioneer(preparedData: SingleChainPreparedData): Promise<ApiResponse> {
return BaseSDK.sendSingleChainOrder({
order: this,
preparedData,
});
}
public async toEVMTypedData() {
if (!isEvmChain(this.chainId)) {
throw new ValidationError('Chain id is not an Ethereum compatible chain');
}
return getEVMSingleChainOrderTypedData(this);
}
public async toSolanaInstructionsByteArray() {
if (this.chainId !== ChainID.Solana) {
throw new ValidationError('Chain id is not Solana');
}
return getSolanaSingleChainOrderInstructions(this);
}
}