UNPKG

@robertprp/intents-sdk

Version:

Shogun Network Intent-based cross-chain swaps SDK

316 lines 12.8 kB
import { sha256, toBytes } from 'viem'; import { isEvmChain, ChainID, chainIdToChainTypeMap } from '../../chains.js'; import { ValidationError } from '../../errors/index.js'; import {} from '../../types/intent.js'; import { CrossChainOrderValidator } from '../../utils/order-validator.js'; import { Parsers } from '../../utils/parsers.js'; import {} from './common.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'; /** * Represents a X-chain swap order * Contains all the information needed to execute the order on both source and destination chains */ export class CrossChainOrder { constructor(params) { /** User's wallet address that initiates the order */ Object.defineProperty(this, "user", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Source chain ID where tokens will be sent from */ Object.defineProperty(this, "sourceChainId", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Token address on the source chain to be swapped */ Object.defineProperty(this, "sourceTokenAddress", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Amount of source tokens to swap */ Object.defineProperty(this, "sourceTokenAmount", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Destination chain ID where tokens will be received */ Object.defineProperty(this, "destinationChainId", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Token address on the destination chain to receive */ Object.defineProperty(this, "destinationTokenAddress", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Minimum amount of destination tokens to receive */ Object.defineProperty(this, "destinationTokenMinAmount", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Recipient wallet address on the destination chain */ Object.defineProperty(this, "destinationAddress", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Minimum amount of stablecoins in the intermediate swap */ Object.defineProperty(this, "minStablecoinAmount", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Timestamp (in seconds) after which the order expires */ Object.defineProperty(this, "deadline", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Extra transfers to be made */ Object.defineProperty(this, "extraTransfers", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Stop loss max out */ Object.defineProperty(this, "stopLossMaxOut", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Take profit min out */ Object.defineProperty(this, "takeProfitMinOut", { enumerable: true, configurable: true, writable: true, value: void 0 }); 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 */ static async create(input) { // 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, }); 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. getRandomPreparedData() { 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'); } } } static async calculateAmountOutMin(input) { 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 */ getExecutionDetails() { return { destChainId: this.destinationChainId, tokenOut: this.destinationTokenAddress, destinationAddress: this.destinationAddress, amountOutMin: this.destinationTokenMinAmount, extraTransfers: this.extraTransfers, }; } executionDetailsHashToBytes() { 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 */ getExecutionDetailsHash() { 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))); } } toIntentRequest(preparedData) { 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 */ toSourceChainData() { let data = { 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 */ toJSON() { return { user: this.user, sourceChainId: this.sourceChainId, sourceTokenAddress: this.sourceTokenAddress, sourceTokenAmount: this.sourceTokenAmount.toString(), destinationChainId: this.destinationChainId, 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, }; } sendToAuctioneer(preparedData) { return BaseSDK.sendCrossChainOrder({ order: this, preparedData, }); } async toEVMTypedData() { if (!isEvmChain(this.sourceChainId)) { throw new ValidationError('Source chain is not EVM'); } return getEVMCrossChainOrderTypedData(this); } async toSolanaInstructionsByteArray() { if (this.sourceChainId !== ChainID.Solana) { throw new ValidationError('Source chain is not Solana'); } return getSolanaCrossChainOrderInstructions(this); } async toSuiTransaction() { if (this.sourceChainId !== ChainID.Sui) { throw new ValidationError('Source chain is not Sui'); } return getSuiOrderTransaction(this); } } //# sourceMappingURL=cross-chain.js.map