UNPKG

@lifi/sdk

Version:

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

367 lines 14.5 kB
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'; /** * 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 = [ // OKX delegation isn't compatible at this moment '36d3CBD83961868398d056EfBf50f5CE15528c0D', ]; function makeDomainSeparator({ name, version, chainId, verifyingContract, withSalt = false, }) { 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, }) { 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) => { 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) => 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, chainId, tokenAddress) => { try { const multicallAddress = await getMulticallAddress(chainId); const contractCalls = [ { address: tokenAddress, abi: eip2612Abi, functionName: 'eip712Domain', }, { address: tokenAddress, abi: eip2612Abi, functionName: 'nonces', args: [client.account.address], }, ]; 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)))); 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, chainId, tokenAddress) => { 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', }, ]; 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)))); 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, { chainId, tokenAddress, spenderAddress, amount }) => { // 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, }; }; //# sourceMappingURL=getNativePermit.js.map