UNPKG

@robertprp/intents-sdk

Version:

Shogun Network Intent-based cross-chain swaps SDK

175 lines (147 loc) 5.82 kB
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); } }