UNPKG

@robertprp/intents-sdk

Version:

Shogun Network Intent-based cross-chain swaps SDK

328 lines (290 loc) 11.8 kB
import { sha256, toBytes, type TypedDataDefinition } from 'viem'; import { isEvmChain, ChainID, chainIdToChainTypeMap, type SupportedChain } from '../../chains.js'; import { ValidationError } from '../../errors/index.js'; import { type ChainPreparedData, type CrossChainUserIntentRequest, type ExecutionDetails, type Hash, type SourceChainData, } from '../../types/intent.js'; import { CrossChainOrderValidator } from '../../utils/order-validator.js'; import { Parsers } from '../../utils/parsers.js'; import { type ExtraTransfer } from './common.js'; import type { ApiResponse } from '../../types/api.js'; import { BaseSDK } from '../sdk.js'; import { getEVMCrossChainOrderTypedData } from '../evm/order-signature.js'; import { getSolanaCrossChainOrderInstructions } from '../solana/order-instructions.js'; import { getSuiOrderTransaction } from '../sui/order-transaction.js'; import { QuoteProvider } from '../../utils/quote/aggregator.js'; export type CreateCrossChainOrderParams = { /** Source chain ID where tokens will be sent from */ sourceChainId: SupportedChain; /** Token address on the source chain to be swapped */ sourceTokenAddress: string; /** Amount of source tokens to swap with decimals */ sourceTokenAmount: bigint; /** Destination chain ID where tokens will be received */ destinationChainId: SupportedChain; /** Token address on the destination chain to receive */ destinationTokenAddress: string; /** Minimum amount of destination tokens to receive */ destinationTokenMinAmount?: bigint; /** Recipient wallet address on the destination chain */ destinationAddress: string; /** Minimum amount of stablecoins in the intermediate swap */ minStablecoinAmount?: bigint; /** Timestamp (in seconds) after which the order expires */ deadline: number; /** Extra transfers to be made */ extraTransfers?: ExtraTransfer[]; /** Stop loss max out */ stopLossMaxOut?: bigint; /** Take profit min out */ takeProfitMinOut?: bigint; }; /** * Represents a X-chain swap order * Contains all the information needed to execute the order on both source and destination chains */ export class CrossChainOrder { /** User's wallet address that initiates the order */ public user: string; /** Source chain ID where tokens will be sent from */ public sourceChainId: SupportedChain; /** Token address on the source chain to be swapped */ public sourceTokenAddress: string; /** Amount of source tokens to swap */ public sourceTokenAmount: bigint; /** Destination chain ID where tokens will be received */ public destinationChainId: SupportedChain; /** Token address on the destination chain to receive */ public destinationTokenAddress: string; /** Minimum amount of destination tokens to receive */ public destinationTokenMinAmount: bigint; /** Recipient wallet address on the destination chain */ public destinationAddress: string; /** Minimum amount of stablecoins in the intermediate swap */ public minStablecoinAmount: bigint; /** Timestamp (in seconds) after which the order expires */ public deadline: number; /** Extra transfers to be made */ public extraTransfers?: ExtraTransfer[]; /** Stop loss max out */ public stopLossMaxOut?: bigint; /** Take profit min out */ public takeProfitMinOut?: bigint; private constructor(params: CreateCrossChainOrderParams & { user: string }) { this.user = params.user; this.sourceChainId = params.sourceChainId; this.sourceTokenAddress = params.sourceTokenAddress; this.sourceTokenAmount = params.sourceTokenAmount; this.destinationChainId = params.destinationChainId; this.destinationTokenAddress = params.destinationTokenAddress; this.destinationTokenMinAmount = params.destinationTokenMinAmount ?? 1n; this.destinationAddress = params.destinationAddress; this.minStablecoinAmount = params.minStablecoinAmount ?? 1n; this.deadline = params.deadline; this.extraTransfers = params.extraTransfers; this.stopLossMaxOut = params.stopLossMaxOut; this.takeProfitMinOut = params.takeProfitMinOut; } /** * Factory method to create and validate a new Order instance * @param input Order parameters * @returns Validated Order instance * @throws {ValidationError} If order validation fails */ public static async create(input: CreateCrossChainOrderParams & { user: string }): Promise<CrossChainOrder> { // Validate first on creation await new CrossChainOrderValidator().validateOrder({ ...input, user: input.user }); const { minStablecoinAmount, destinationTokenMinAmount } = await this.calculateAmountOutMin(input); const order = new CrossChainOrder({ ...input, minStablecoinAmount: minStablecoinAmount, destinationTokenMinAmount: destinationTokenMinAmount, user: input.user, }); if (isEvmChain(order.sourceChainId)) { return order; } const randomPreparedData = order.getRandomPreparedData(); const intentRequest = order.toIntentRequest(randomPreparedData); await BaseSDK.validateCrossChainOrder(intentRequest); return order; } /// This is needed because API requires the prepared data to be sent /// 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(): ChainPreparedData { const chainId = this.sourceChainId; const chainType = chainIdToChainTypeMap[chainId]; switch (chainType) { case 'EVM': { return { nonce: String(Math.floor(Math.random() * 10000000)), signature: '0x0000000000000000000000000000000000000000000000000000000000000000', }; } case 'Solana': { return { orderPubkey: 'DFNAjFAvS4GF98Tp1kiyLvEHM3wjGXibCfF86nnmhuVc' }; } case 'Sui': { const digest = 'FQWndwYJhNQUoHyvR8UuhGURC2EKx9eWErFm9Tc2DggF'; return { transactionHash: digest }; } default: { throw new Error('Chain type not supported'); } } } private static async calculateAmountOutMin( input: CreateCrossChainOrderParams, ): Promise<{ minStablecoinAmount: bigint; destinationTokenMinAmount: bigint }> { const { destinationTokenMinAmount, stopLossMaxOut, minStablecoinAmount } = input; if (stopLossMaxOut !== undefined) { return { destinationTokenMinAmount: 1n, minStablecoinAmount: 1n, }; } if (!minStablecoinAmount || !destinationTokenMinAmount) { const quote = await QuoteProvider.getQuote({ sourceChainId: input.sourceChainId, tokenIn: input.sourceTokenAddress, amount: input.sourceTokenAmount, destChainId: input.destinationChainId, tokenOut: input.destinationTokenAddress, }); return { destinationTokenMinAmount: quote.estimatedAmountOutReduced, minStablecoinAmount: quote.estimatedAmountInAsMinStablecoinAmount, }; } return { destinationTokenMinAmount, minStablecoinAmount, }; } /** * Gets the execution details for the destination chain * These details are used to complete the order on the destination chain * @returns Structured execution details object */ public getExecutionDetails(): ExecutionDetails { return { destChainId: this.destinationChainId, tokenOut: this.destinationTokenAddress, destinationAddress: this.destinationAddress, amountOutMin: this.destinationTokenMinAmount, extraTransfers: this.extraTransfers, }; } public executionDetailsHashToBytes(): Uint8Array { const executionDetailsHash = this.getExecutionDetailsHash().slice(2); const executionHashByteSlice = Buffer.from(executionDetailsHash, 'hex'); return new Uint8Array(executionHashByteSlice); } /** * Generates a cryptographic hash of the execution details * This hash is used to verify order integrity across chains * @returns SHA-256 hash of the execution details as a 0x-prefixed hex string * @throws {ValidationError} If hash generation fails */ public getExecutionDetailsHash(): Hash { try { const executionDetails = this.getExecutionDetails(); const executionDetailsString = JSON.stringify(executionDetails, Parsers.bigIntReplacer); const bytes = toBytes(executionDetailsString); return sha256(bytes); } catch (error) { throw new ValidationError( 'Failed to generate execution details hash', error instanceof Error ? error : new Error(String(error)), ); } } public toIntentRequest(preparedData: ChainPreparedData): CrossChainUserIntentRequest { const sourceChain = this.sourceChainId; const sourceChainType = chainIdToChainTypeMap[sourceChain]; const executionDetails = JSON.stringify(this.getExecutionDetails(), Parsers.bigIntReplacer); return { genericData: this.toSourceChainData(), executionDetails, chainSpecificData: { [sourceChainType]: preparedData, }, }; } /** * Converts the order to the format required for source chain processing * Used when sending the order to the auctioneer for execution * @returns Object containing source chain-specific data */ public toSourceChainData() { let data: SourceChainData = { user: this.user, srcChainId: this.sourceChainId, tokenIn: this.sourceTokenAddress, amountIn: this.sourceTokenAmount, minStablecoinsAmount: this.minStablecoinAmount, deadline: this.deadline, executionDetailsHash: this.getExecutionDetailsHash(), extraTransfers: this.extraTransfers, }; if (this.stopLossMaxOut !== undefined) { data.stopLossMaxOut = this.stopLossMaxOut; } if (this.takeProfitMinOut !== undefined) { data.takeProfitMinOut = this.takeProfitMinOut; } return data; } /** * Serializes the order to a JSON-compatible object * Converts bigint values to strings to ensure proper JSON serialization * @returns JSON-serializable representation of the order */ public toJSON() { return { user: this.user, sourceChainId: this.sourceChainId as number, sourceTokenAddress: this.sourceTokenAddress, sourceTokenAmount: this.sourceTokenAmount.toString(), destinationChainId: this.destinationChainId as number, destinationTokenAddress: this.destinationTokenAddress, destinationTokenMinAmount: this.destinationTokenMinAmount.toString(), destinationAddress: this.destinationAddress, minStablecoinAmount: this.minStablecoinAmount.toString(), deadline: this.deadline, executionDetailsHash: this.getExecutionDetailsHash(), extraTransfers: this.extraTransfers, stopLossMaxOut: this.stopLossMaxOut, takeProfitMinOut: this.takeProfitMinOut, }; } public sendToAuctioneer(preparedData: ChainPreparedData): Promise<ApiResponse> { return BaseSDK.sendCrossChainOrder({ order: this, preparedData, }); } public async toEVMTypedData(): Promise<{ orderTypedData: TypedDataDefinition; nonce: bigint }> { if (!isEvmChain(this.sourceChainId)) { throw new ValidationError('Source chain is not EVM'); } return getEVMCrossChainOrderTypedData(this); } public async toSolanaInstructionsByteArray() { if (this.sourceChainId !== ChainID.Solana) { throw new ValidationError('Source chain is not Solana'); } return getSolanaCrossChainOrderInstructions(this); } public async toSuiTransaction() { if (this.sourceChainId !== ChainID.Sui) { throw new ValidationError('Source chain is not Sui'); } return getSuiOrderTransaction(this); } }