@robertprp/intents-sdk
Version:
Shogun Network Intent-based cross-chain swaps SDK
155 lines (125 loc) • 5.46 kB
text/typescript
import { type Address, getContract, isAddress as isEvmAddress } from 'viem';
import { ChainID, type SupportedEvmChain } from '../../chains.js';
import { isNativeEvmToken, MAX_UINT_128 } from '../../constants.js';
import { InsufficientBalanceError, ValidationError } from '../../errors/index.js';
import { BaseValidator } from '../../utils/base-validator.js';
import type { CreateCrossChainOrderParams } from '../orders/cross-chain.js';
import { ERC20ABI } from './abi/erc20.js';
import { ChainProvider } from './chain-provider.js';
import type { CreateSingleChainOrderParams } from '../orders/single-chain.js';
import type { CreateDcaSingleChainOrderParams } from '../orders/dca-single-chain.js';
/**
* Chain-specific validator implementations
*/
export class EvmValidator extends BaseValidator {
isValidAddress(address: string): boolean {
return isEvmAddress(address);
}
isValidTokenAddress(tokenAddress: string): boolean {
return isEvmAddress(tokenAddress);
}
isValidAmount(amount: bigint): boolean {
return amount > 0n && amount < MAX_UINT_128;
}
protected getChainName(): string {
return 'EVM';
}
public async validateCrossChainOrderFeasability(
order: CreateCrossChainOrderParams & { user: string },
): Promise<void> {
const chain = order.sourceChainId as SupportedEvmChain;
const client = ChainProvider.getClient(chain);
const user = order.user as Address;
if (isNativeEvmToken(order.sourceTokenAddress)) {
throw new ValidationError('Native token is not supported.');
}
if (order.sourceChainId === ChainID.Hyperliquid && isNativeEvmToken(order.destinationTokenAddress)) {
throw new ValidationError('HYPE native token as destination address is not supported.');
}
const ERC20Contract = getContract({
address: order.sourceTokenAddress as Address,
abi: ERC20ABI,
client: {
public: client,
chain,
},
});
const balance = await ERC20Contract.read.balanceOf([user]);
if (balance < order.sourceTokenAmount) {
throw new InsufficientBalanceError(
`Insufficient balance for token ${order.sourceTokenAddress}. Current balance: ${balance}. Required balance: ${order.sourceTokenAmount}`,
);
}
// Temporary allowance turn off. This should be handled in the frontend.
// const permit2Address = PERMIT2_ADDRESS[chain];
// const allowance = await ERC20Contract.read.allowance([user, permit2Address]);
// if (allowance < order.sourceTokenAmount) {
// throw new InsufficientAllowanceError(
// `Insufficient allowance for token ${order.sourceTokenAddress} on permit2 address ${permit2Address}. Current allowance: ${allowance}. Required allowance: ${order.sourceTokenAmount}`,
// );
// }
}
override async validateSingleChainOrderFeasability(
order: CreateSingleChainOrderParams & { user: string },
): Promise<void> {
const chain = order.chainId as SupportedEvmChain;
const validSingleChainChains = [ChainID.Hyperliquid, ChainID.Base];
if (!validSingleChainChains.includes(chain)) {
throw new ValidationError(`Chain ${chain} is not supported for single chain orders`);
}
if (order.chainId === ChainID.Hyperliquid && isNativeEvmToken(order.tokenOut)) {
throw new ValidationError('HYPE native token as destination address is not supported.');
}
const client = ChainProvider.getClient(chain);
const user = order.user as Address;
if (isNativeEvmToken(order.tokenIn)) {
throw new ValidationError('Native token is not supported.');
}
const ERC20Contract = getContract({
address: order.tokenIn as Address,
abi: ERC20ABI,
client: {
public: client,
chain,
},
});
const balance = await ERC20Contract.read.balanceOf([user]);
if (balance < order.amountIn) {
throw new InsufficientBalanceError(
`Insufficient balance for token ${order.tokenIn}. Current balance: ${balance}. Required balance: ${order.amountIn}`,
);
}
// const permit2Address = PERMIT2_ADDRESS[chain];
// const allowance = await ERC20Contract.read.allowance([user, permit2Address]);
// if (allowance < order.amountIn) {
// throw new InsufficientAllowanceError(
// `Insufficient allowance for token ${order.tokenIn} on permit2 address ${permit2Address}. Current allowance: ${allowance}. Required allowance: ${order.amountIn}`,
// );
// }
}
override async validateDcaSingleChainOrderFeasability(
order: CreateDcaSingleChainOrderParams & { user: string },
): Promise<void> {
const chain = order.chainId as SupportedEvmChain;
const client = ChainProvider.getClient(chain);
const user = order.user as Address;
if (isNativeEvmToken(order.tokenIn)) {
throw new ValidationError('Native token is not supported for DCA orders.');
}
const ERC20Contract = getContract({
address: order.tokenIn as Address,
abi: ERC20ABI,
client: {
public: client,
chain,
},
});
const totalAmountIn = order.amountInPerInterval * BigInt(order.totalIntervals);
const balance = await ERC20Contract.read.balanceOf([user]);
if (balance < totalAmountIn) {
throw new InsufficientBalanceError(
`Insufficient balance for DCA order. Token: ${order.tokenIn}. Current balance: ${balance}. Required total: ${totalAmountIn}`,
);
}
}
}