UNPKG

@openocean.finance/widget-sdk

Version:

OpenOcean Any-to-Any Cross-Chain-Swap SDK

276 lines (253 loc) 7.12 kB
import { encodeAbiParameters, keccak256, pad, parseAbiParameters, toBytes, toHex, } from 'viem' import type { Address, Client, Hex } from 'viem' import type { TypedDataDomain } from 'viem' import { multicall, readContract } from 'viem/actions' import { eip2612Abi, eip2612Types } from '../abi.js' import { getMulticallAddress } from '../utils.js' import type { NativePermitData } from './types.js' /** * EIP-712 domain typehash with chainId * @link https://eips.ethereum.org/EIPS/eip-712#specification * * keccak256(toBytes( * 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' * )) */ const EIP712_DOMAIN_TYPEHASH = '0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f' as Hex /** * EIP-712 domain typehash with salt (e.g. USDC.e on Polygon) * @link https://eips.ethereum.org/EIPS/eip-712#specification * * keccak256(toBytes( * 'EIP712Domain(string name,string version,address verifyingContract,bytes32 salt)' * )) */ const EIP712_DOMAIN_TYPEHASH_WITH_SALT = '0x36c25de3e541d5d970f66e4210d728721220fff5c077cc6cd008b3a0c62adab7' as Hex 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) } // TODO: Add support for EIP-5267 when adoption increases // This EIP provides a standard way to query domain separator and permit type hash // via eip712Domain() function, which would simplify permit validation // https://eips.ethereum.org/EIPS/eip-5267 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: {}, } } /** * 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: number, tokenAddress: Address, spenderAddress: Address, amount: bigint ): Promise<NativePermitData | undefined> => { try { 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: 'nonces', args: [client.account!.address], }, { address: tokenAddress, abi: eip2612Abi, functionName: 'version', }, ] as const if (multicallAddress) { const [nameResult, domainSeparatorResult, noncesResult, versionResult] = await multicall(client, { contracts: contractCalls, multicallAddress, }) if ( nameResult.status !== 'success' || domainSeparatorResult.status !== 'success' || noncesResult.status !== 'success' || !nameResult.result || !domainSeparatorResult.result || noncesResult.result === undefined ) { return undefined } const { isValid, domain } = validateDomainSeparator({ name: nameResult.result, version: versionResult.result ?? '1', chainId, verifyingContract: tokenAddress, domainSeparator: domainSeparatorResult.result, }) if (!isValid) { return undefined } const message = { owner: client.account!.address, spender: spenderAddress, value: amount.toString(), nonce: noncesResult.result.toString(), deadline: BigInt(Math.floor(Date.now() / 1000) + 30 * 60).toString(), // 30 minutes } return { primaryType: 'Permit', domain, types: eip2612Types, message, } } const [nameResult, domainSeparatorResult, noncesResult, versionResult] = (await Promise.allSettled( contractCalls.map((call) => readContract(client, call)) )) as [ PromiseSettledResult<string>, 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' const { isValid, domain } = validateDomainSeparator({ name, version, chainId, verifyingContract: tokenAddress, domainSeparator: domainSeparatorResult.value, }) if (!isValid) { return undefined } const message = { owner: client.account!.address, spender: spenderAddress, value: amount.toString(), nonce: noncesResult.value.toString(), deadline: BigInt(Math.floor(Date.now() / 1000) + 30 * 60).toString(), // 30 minutes } return { primaryType: 'Permit', domain, types: eip2612Types, message, } } catch { return undefined } }