UNPKG

@robertprp/intents-sdk

Version:

Shogun Network Intent-based cross-chain swaps SDK

248 lines (206 loc) 8.33 kB
import { privateKeyToAccount } from 'viem/accounts'; import type { EVMConfig } from '../../config.js'; import { BaseSDK } from '../sdk.js'; import { EVMIntentProvider } from './intent-provider.js'; import { type Address, type Hex } from 'viem'; import { PERMIT2_ADDRESS, CROSS_CHAIN_GUARD_ADDRESSES, SINGLE_CHAIN_GUARD_ADDRESSES, MAX_UINT_256, } from '../../constants.js'; import type { ExtraTransfer } from '../orders/common.js'; import type { CrossChainOrder } from '../orders/cross-chain.js'; import type { SingleChainOrder } from '../orders/single-chain.js'; import type { CrossChainOrderPrepared, SingleChainOrderPrepared } from '../../types/intent.js'; import type { ApiUserOrders } from '../../types/api.js'; import { fetchJWTToken, fetchSiweMessage } from '../../auth/siwe.js'; import { fetchUserOrders } from '../orders/api/fetch.js'; type CancelSingleChainOrderParams = { orderId: string; user: string; tokenIn: string; amountIn: bigint; requestedOutput: ExtraTransfer; extraTransfers: ExtraTransfer[]; encodedExternalCallData: string; deadline: number; nonce: bigint; }; type CancelCrossChainOrderParams = { orderId: string; user: string; tokenIn: string; amountIn: bigint; srcChainId: number; deadline: number; minStablecoinsAmount: bigint; executionDetailsHash: string; nonce: bigint; }; /** * Handles EVM-specific aspects of cross-chain swaps for Ethereum-compatible chains: */ export class EVMSDK extends BaseSDK { private readonly config: EVMConfig; private readonly evmIntentProvider: EVMIntentProvider; private token?: string; constructor(config: EVMConfig) { super(); this.config = config; this.evmIntentProvider = new EVMIntentProvider(config); } public async cancelCrossChainOrder(params: CancelCrossChainOrderParams): Promise<string> { const chainId = this.config.chainId; const auctioneerAddress = CROSS_CHAIN_GUARD_ADDRESSES[chainId] as Hex; const userAddress = (await this.getUserAddress()) as Hex; const auctioneerContract = this.evmIntentProvider.provider.getCrossChainAuctioneerContract(auctioneerAddress); const permit2Contract = this.evmIntentProvider.provider.getPermit2Contract(PERMIT2_ADDRESS[chainId]); const orderIdHex = params.orderId as Hex; let [initialized, deactivated] = await auctioneerContract.read.orderData([orderIdHex]); let txHash: Hex; if (initialized) { if (deactivated) { throw new Error('Order is already deactivated'); } const orderInfo = { user: params.user as Hex, tokenIn: params.tokenIn as Hex, srcChainId: params.srcChainId, deadline: params.deadline, amountIn: BigInt(params.amountIn), minStablecoinsAmount: BigInt(params.minStablecoinsAmount), executionDetailsHash: params.executionDetailsHash as Hex, nonce: BigInt(params.nonce!), }; const tx = await auctioneerContract.write.cancelOrder([orderInfo]); txHash = tx; } else { const nonce = BigInt(params.nonce); let nonceWordPos = nonce >> 8n; let nonceBitPos = nonce - nonceWordPos * 256n; let mask = 1n << nonceBitPos; const currentNonceBitmap = await permit2Contract.read.nonceBitmap([userAddress, nonceWordPos]); if ((currentNonceBitmap & (1n << nonceBitPos)) !== 0n) { throw new Error('Nonce is already invalidated'); } const tx = await permit2Contract.write.invalidateUnorderedNonces([nonceWordPos, mask]); txHash = tx; } return txHash; } public async cancelSingleChainOrder(params: CancelSingleChainOrderParams): Promise<string> { const chainId = this.config.chainId; const auctioneerAddress = SINGLE_CHAIN_GUARD_ADDRESSES[chainId] as Hex; const userAddress = (await this.getUserAddress()) as Hex; const auctioneerContract = this.evmIntentProvider.provider.getSingleChainAuctioneerContract(auctioneerAddress); const orderHash = params.orderId as Hex; const wasManuallyInitialized = await auctioneerContract.read.orderManuallyInitialized([orderHash]); let txHash: Hex; if (wasManuallyInitialized) { txHash = await auctioneerContract.write.cancelManuallyCreatedOrder([ { amountIn: params.amountIn, tokenIn: params.tokenIn as Hex, deadline: params.deadline as number, nonce: params.nonce, encodedExternalCallData: params.encodedExternalCallData as Hex, extraTransfers: params.extraTransfers.map((transfer) => { return { amount: transfer.amount, receiver: transfer.receiver as Hex, token: transfer.token as Hex, }; }), requestedOutput: { amount: params.requestedOutput.amount, receiver: params.requestedOutput.receiver as Hex, token: params.requestedOutput.token as Hex, }, user: params.user as Hex, }, ]); } else { const permit2Contract = this.evmIntentProvider.provider.getPermit2Contract(PERMIT2_ADDRESS[chainId]); const nonce = BigInt(params.nonce); const nonceWordPos = nonce >> 8n; const nonceBitPos = nonce - nonceWordPos * 256n; const mask = 1n << nonceBitPos; // Check if nonce is already invalidated const currentNonceBitmap = await permit2Contract.read.nonceBitmap([userAddress, nonceWordPos]); if ((currentNonceBitmap & (1n << nonceBitPos)) !== 0n) { throw new Error('Nonce is already invalidated'); } txHash = await permit2Contract.write.invalidateUnorderedNonces([nonceWordPos, mask]); } return txHash; } public async authenticate(token?: string): Promise<string> { const chainId = this.config.chainId; const wallet = await this.getUserAddress(); const response = await fetchSiweMessage({ chainId, wallet, }); const message = response.data!; // Sign the message using the wallet client const signature = await this.evmIntentProvider.provider.walletClient.signMessage({ message, }); const jwt = await fetchJWTToken( { message, signature, }, token, ); const newToken = jwt.data!; return newToken; } public setToken(token: string) { this.token = token; return this; } public override async getOrders(): Promise<ApiUserOrders> { if (!this.token) { throw new Error('No token provided'); } const orders = await fetchUserOrders(this.token); return orders; } /** * Gets the user's Ethereum address derived from their private key * @returns Promise resolving to the user's Ethereum address as a 0x-prefixed string */ public async getUserAddress(): Promise<string> { return privateKeyToAccount(this.config.privateKey).address; } protected async approveAllowanceIfNeeded(tokenAddress: Address, amount: bigint): Promise<void> { const permit2Address = PERMIT2_ADDRESS[this.config.chainId]; const ERC20Contract = this.evmIntentProvider.provider.getERC20Contract(tokenAddress); const userAddress = (await this.getUserAddress()) as Address; const allowance = await ERC20Contract.read.allowance([userAddress, permit2Address]); if (allowance < amount) { await ERC20Contract.write.approve([permit2Address, MAX_UINT_256]); } } /** * Prepares an EVM order for submission * * This method: * 1. Validates token balances and allowances * 2. Creates and signs the EIP-712 typed data for Permit2 * 3. Prepares the order for submission to the auctioneer * * @param order The validated order to prepare * @returns Promise resolving to a prepared order with EVM-specific signature data */ protected override async prepareCrossChainOrder(order: CrossChainOrder): Promise<CrossChainOrderPrepared> { await this.approveAllowanceIfNeeded(order.sourceTokenAddress as Address, order.sourceTokenAmount); return this.evmIntentProvider.prepareCrossChainOrder(order); } protected async prepareSingleChainOrder(order: SingleChainOrder): Promise<SingleChainOrderPrepared> { await this.approveAllowanceIfNeeded(order.tokenIn as Address, order.amountIn); return this.evmIntentProvider.prepareSingleChainOrder(order); } }