UNPKG

@lifi/sdk

Version:

LI.FI Any-to-Any Cross-Chain-Swap SDK

530 lines (483 loc) 14.1 kB
import type { Address, Client, Hex, TypedDataDomain } from 'viem' import { encodeAbiParameters, keccak256, pad, parseAbiParameters, toBytes, toHex, zeroHash, } from 'viem' import { getCode, multicall, readContract } from 'viem/actions' import { eip2612Abi } from '../abi.js' import { getActionWithFallback } from '../getActionWithFallback.js' import { getMulticallAddress, isDelegationDesignatorCode } from '../utils.js' import { DAI_LIKE_PERMIT_TYPEHASH, EIP712_DOMAIN_TYPEHASH, EIP712_DOMAIN_TYPEHASH_WITH_SALT, eip2612Types, } from './constants.js' import type { NativePermitData } from './types.js' /** * Blocklist of EIP-7702 delegation addresses that should not use native permits * These delegations may have incompatibilities or security concerns with permit signatures * Addresses should be lowercase without 0x prefix for case-insensitive matching */ const EIP7702_DELEGATION_BLOCKLIST: string[] = [ // OKX delegation isn't compatible at this moment '36d3CBD83961868398d056EfBf50f5CE15528c0D', ] type GetNativePermitParams = { chainId: number tokenAddress: Address spenderAddress: Address amount: bigint } function makeDomainSeparator({ name, version, chainId, verifyingContract, withSalt = false, }: { name: string version: string chainId: number verifyingContract: Address withSalt?: boolean }): Hex { const nameHash = keccak256(toBytes(name)) const versionHash = keccak256(toBytes(version)) const encoded = withSalt ? encodeAbiParameters( parseAbiParameters('bytes32, bytes32, bytes32, address, bytes32'), [ EIP712_DOMAIN_TYPEHASH_WITH_SALT, nameHash, versionHash, verifyingContract, pad(toHex(chainId), { size: 32 }), ] ) : encodeAbiParameters( parseAbiParameters('bytes32, bytes32, bytes32, uint256, address'), [ EIP712_DOMAIN_TYPEHASH, nameHash, versionHash, BigInt(chainId), verifyingContract, ] ) return keccak256(encoded) } function validateDomainSeparator({ name, version, chainId, verifyingContract, domainSeparator, }: { name: string version: string chainId: number verifyingContract: Address domainSeparator: Hex }): { isValid: boolean; domain: TypedDataDomain } { if (!name || !domainSeparator) { return { isValid: false, domain: {}, } } for (const withSalt of [false, true]) { const computedDS = makeDomainSeparator({ name, version, chainId, verifyingContract, withSalt, }) if (domainSeparator.toLowerCase() === computedDS.toLowerCase()) { return { isValid: true, domain: withSalt ? { name, version, verifyingContract, salt: pad(toHex(chainId), { size: 32 }), } : { name, version, chainId, verifyingContract, }, } } } return { isValid: false, domain: {}, } } /** * Checks if the account can use native permits based on its code. * Returns true if: * 1. Account has no code (EOA) * 2. Account is EOA and has EIP-7702 delegation designator code (and not in blocklist) * * @param client - The Viem client instance * @returns Promise<boolean> - Whether the account can use native permits */ const canAccountUseNativePermits = async (client: Client): Promise<boolean> => { try { const accountCode = await getActionWithFallback( client, getCode, 'getCode', { address: client.account!.address, } ) // If no code (0x or undefined), it's an EOA - can use native permits if (!accountCode || accountCode === '0x') { return true } // If has code but it's EIP-7702 delegation designator - check blocklist if (isDelegationDesignatorCode(accountCode)) { // Check if the code contains any blocklisted delegation address const codeLC = accountCode.toLowerCase() const isBlocked = EIP7702_DELEGATION_BLOCKLIST.some( (blockedAddress: string) => codeLC.includes(blockedAddress.toLowerCase()) ) if (isBlocked) { return false } return true } // If has code but not EIP-7702 delegation - cannot use native permits // Smart Accounts like Kernel (ZeroDev) can't produce ECDSA signatures, so we can't use native permits in current implementation return false } catch { // If we can't check the code, assume it's not safe to use native permits return false } } /** * Attempts to retrieve contract data using EIP-5267 eip712Domain() function * @link https://eips.ethereum.org/EIPS/eip-5267 * @param client - The Viem client instance * @param chainId - The chain ID * @param tokenAddress - The token contract address * @returns Contract data if EIP-5267 is supported, undefined otherwise */ const getEIP712DomainData = async ( client: Client, chainId: number, tokenAddress: Address ) => { try { const multicallAddress = await getMulticallAddress(chainId) const contractCalls = [ { address: tokenAddress, abi: eip2612Abi, functionName: 'eip712Domain', }, { address: tokenAddress, abi: eip2612Abi, functionName: 'nonces', args: [client.account!.address], }, ] as const if (multicallAddress) { try { const [eip712DomainResult, noncesResult] = await getActionWithFallback( client, multicall, 'multicall', { contracts: contractCalls, multicallAddress, } ) if ( eip712DomainResult.status !== 'success' || noncesResult.status !== 'success' || !eip712DomainResult.result || noncesResult.result === undefined ) { // Fall back to individual calls if multicall fails throw new Error('EIP-5267 multicall failed') } const [, name, version, tokenChainId, verifyingContract, salt] = eip712DomainResult.result if ( Number(tokenChainId) !== chainId || verifyingContract.toLowerCase() !== tokenAddress.toLowerCase() ) { return undefined } // Build domain object directly from EIP-5267 data // Use the actual salt value returned by EIP-5267 - this is the canonical salt that the contract uses const hasSalt = salt !== zeroHash const domain = hasSalt ? { name, version, verifyingContract: tokenAddress, salt, } : { name, version, chainId, verifyingContract: tokenAddress, } return { name, version, domain, permitTypehash: undefined, // EIP-5267 doesn't provide permit typehash directly nonce: noncesResult.result, } } catch { // Fall through to individual calls } } // Fallback to individual contract calls const [eip712DomainResult, noncesResult] = (await Promise.allSettled( contractCalls.map((call) => getActionWithFallback(client, readContract, 'readContract', call) ) )) as [ PromiseSettledResult< [Hex, string, string, bigint, Address, Hex, bigint[]] >, PromiseSettledResult<bigint>, ] if ( eip712DomainResult.status !== 'fulfilled' || noncesResult.status !== 'fulfilled' ) { return undefined } const [, name, version, tokenChainId, verifyingContract, salt] = eip712DomainResult.value if ( Number(tokenChainId) !== chainId || verifyingContract.toLowerCase() !== tokenAddress.toLowerCase() ) { return undefined } // Build domain object directly from EIP-5267 data // Use the actual salt value returned by EIP-5267 - this is the canonical salt that the contract uses const hasSalt = salt !== zeroHash const domain = hasSalt ? { name, version, verifyingContract: tokenAddress, salt, } : { name, version, chainId, verifyingContract: tokenAddress, } return { name, version, domain, permitTypehash: undefined, // EIP-5267 doesn't provide permit typehash directly nonce: noncesResult.value, } } catch { return undefined } } const getContractData = async ( client: Client, chainId: number, tokenAddress: Address ) => { try { // First try EIP-5267 approach - returns domain object directly const eip5267Data = await getEIP712DomainData(client, chainId, tokenAddress) if (eip5267Data) { return eip5267Data } // Fallback to legacy approach - validates and returns domain object const multicallAddress = await getMulticallAddress(chainId) const contractCalls = [ { address: tokenAddress, abi: eip2612Abi, functionName: 'name', }, { address: tokenAddress, abi: eip2612Abi, functionName: 'DOMAIN_SEPARATOR', }, { address: tokenAddress, abi: eip2612Abi, functionName: 'PERMIT_TYPEHASH', }, { address: tokenAddress, abi: eip2612Abi, functionName: 'nonces', args: [client.account!.address], }, { address: tokenAddress, abi: eip2612Abi, functionName: 'version', }, ] as const if (multicallAddress) { try { const [ nameResult, domainSeparatorResult, permitTypehashResult, noncesResult, versionResult, ] = await getActionWithFallback(client, multicall, 'multicall', { contracts: contractCalls, multicallAddress, }) if ( nameResult.status !== 'success' || domainSeparatorResult.status !== 'success' || noncesResult.status !== 'success' || !nameResult.result || !domainSeparatorResult.result || noncesResult.result === undefined ) { // Fall back to individual calls if multicall fails throw new Error('Multicall failed') } // Validate domain separator and create domain object const { isValid, domain } = validateDomainSeparator({ name: nameResult.result, version: versionResult.result ?? '1', chainId, verifyingContract: tokenAddress, domainSeparator: domainSeparatorResult.result, }) if (!isValid) { return undefined } return { name: nameResult.result, domain, permitTypehash: permitTypehashResult.result, nonce: noncesResult.result, version: versionResult.result ?? '1', } } catch { // Fall through to individual calls } } const [ nameResult, domainSeparatorResult, permitTypehashResult, noncesResult, versionResult, ] = (await Promise.allSettled( contractCalls.map((call) => getActionWithFallback(client, readContract, 'readContract', call) ) )) as [ PromiseSettledResult<string>, PromiseSettledResult<Hex>, PromiseSettledResult<Hex>, PromiseSettledResult<bigint>, PromiseSettledResult<string>, ] if ( nameResult.status !== 'fulfilled' || domainSeparatorResult.status !== 'fulfilled' || noncesResult.status !== 'fulfilled' ) { return undefined } const name = nameResult.value const version = versionResult.status === 'fulfilled' ? versionResult.value : '1' // Validate domain separator and create domain object const { isValid, domain } = validateDomainSeparator({ name, version, chainId, verifyingContract: tokenAddress, domainSeparator: domainSeparatorResult.value, }) if (!isValid) { return undefined } return { name, domain, permitTypehash: permitTypehashResult.status === 'fulfilled' ? permitTypehashResult.value : undefined, nonce: noncesResult.value, version, } } catch { return undefined } } /** * Retrieves native permit data (EIP-2612) for a token on a specific chain * @link https://eips.ethereum.org/EIPS/eip-2612 * @param client - The Viem client instance * @param chain - The extended chain object containing chain details * @param tokenAddress - The address of the token to check for permit support * @returns {Promise<NativePermitData>} Object containing permit data including name, version, nonce and support status */ export const getNativePermit = async ( client: Client, { chainId, tokenAddress, spenderAddress, amount }: GetNativePermitParams ): Promise<NativePermitData | undefined> => { // Check if the account can use native permits (EOA or EIP-7702 delegated account) const canUsePermits = await canAccountUseNativePermits(client) if (!canUsePermits) { return undefined } const contractData = await getContractData(client, chainId, tokenAddress) if (!contractData) { return undefined } // We don't support DAI-like permits yet (e.g. DAI on Ethereum) // https://eips.ethereum.org/EIPS/eip-2612#backwards-compatibility if (contractData.permitTypehash === DAI_LIKE_PERMIT_TYPEHASH) { return undefined } const deadline = BigInt(Math.floor(Date.now() / 1000) + 30 * 60).toString() // 30 minutes const message = { owner: client.account!.address, spender: spenderAddress, value: amount.toString(), nonce: contractData.nonce.toString(), deadline, } return { primaryType: 'Permit', domain: contractData.domain, types: eip2612Types, message, } }