UNPKG

@lifi/sdk

Version:

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

219 lines 10.5 kB
import { signTypedData } from 'viem/actions'; import { getAction } from 'viem/utils'; import { MaxUint256 } from '../../constants.js'; import { getActionWithFallback } from './getActionWithFallback.js'; import { getAllowance } from './getAllowance.js'; import { parseEVMErrors } from './parseEVMErrors.js'; import { getNativePermit } from './permits/getNativePermit.js'; import { isNativePermitValid } from './permits/isNativePermitValid.js'; import { setAllowance } from './setAllowance.js'; import { getDomainChainId } from './utils.js'; import { waitForTransactionReceipt } from './waitForTransactionReceipt.js'; export const checkAllowance = async ({ checkClient, chain, step, statusManager, executionOptions, allowUserInteraction = false, batchingSupported = false, permit2Supported = false, disableMessageSigning = false, }) => { let sharedProcess; let signedTypedData = []; try { // First, try to sign all permits in step.typedData const permitTypedData = step.typedData?.filter((typedData) => typedData.primaryType === 'Permit'); if (!disableMessageSigning && permitTypedData?.length) { sharedProcess = statusManager.findOrCreateProcess({ step, type: 'PERMIT', chainId: step.action.fromChainId, }); signedTypedData = sharedProcess.signedTypedData ?? signedTypedData; for (const typedData of permitTypedData) { // Check if we already have a valid permit for this chain and requirements const signedTypedDataForChain = signedTypedData.find((signedTypedData) => isNativePermitValid(signedTypedData, typedData)); if (signedTypedDataForChain) { // Skip signing if we already have a valid permit continue; } sharedProcess = statusManager.updateProcess(step, sharedProcess.type, 'ACTION_REQUIRED'); if (!allowUserInteraction) { return { status: 'ACTION_REQUIRED' }; } const typedDataChainId = getDomainChainId(typedData.domain) || step.action.fromChainId; // Switch to the permit's chain if needed const permitClient = await checkClient(step, sharedProcess, typedDataChainId); if (!permitClient) { return { status: 'ACTION_REQUIRED' }; } const signature = await getAction(permitClient, signTypedData, 'signTypedData')({ account: permitClient.account, domain: typedData.domain, types: typedData.types, primaryType: typedData.primaryType, message: typedData.message, }); const signedPermit = { ...typedData, signature, }; signedTypedData.push(signedPermit); sharedProcess = statusManager.updateProcess(step, sharedProcess.type, 'ACTION_REQUIRED', { signedTypedData, }); } statusManager.updateProcess(step, sharedProcess.type, 'DONE', { signedTypedData, }); // Check if there's a signed permit for the source transaction chain const matchingPermit = signedTypedData.find((signedTypedData) => getDomainChainId(signedTypedData.domain) === step.action.fromChainId); if (matchingPermit) { return { status: 'NATIVE_PERMIT', data: signedTypedData, }; } } // Find existing or create new allowance process sharedProcess = statusManager.findOrCreateProcess({ step, type: 'TOKEN_ALLOWANCE', chainId: step.action.fromChainId, }); const updatedClient = await checkClient(step, sharedProcess); if (!updatedClient) { return { status: 'ACTION_REQUIRED' }; } // Handle existing pending transaction if (sharedProcess.txHash && sharedProcess.status !== 'DONE') { await waitForApprovalTransaction(updatedClient, sharedProcess.txHash, sharedProcess.type, step, chain, statusManager); return { status: 'DONE', data: signedTypedData }; } // Start new allowance check statusManager.updateProcess(step, sharedProcess.type, 'STARTED'); const spenderAddress = permit2Supported ? chain.permit2 : step.estimate.approvalAddress; const fromAmount = BigInt(step.action.fromAmount); const approved = await getAllowance(updatedClient, step.action.fromToken.address, updatedClient.account.address, spenderAddress); // Return early if already approved if (fromAmount <= approved) { statusManager.updateProcess(step, sharedProcess.type, 'DONE'); return { status: 'DONE', data: signedTypedData }; } // Check if proxy contract is available and message signing is not disabled, also not available for atomic batch const isNativePermitAvailable = !!chain.permit2Proxy && !batchingSupported && !disableMessageSigning; let nativePermitData; if (isNativePermitAvailable) { nativePermitData = await getActionWithFallback(updatedClient, getNativePermit, 'getNativePermit', { chainId: chain.id, tokenAddress: step.action.fromToken.address, spenderAddress: chain.permit2Proxy, amount: fromAmount, }); } if (isNativePermitAvailable && nativePermitData) { signedTypedData = signedTypedData.length ? signedTypedData : sharedProcess.signedTypedData || []; // Check if we already have a valid permit for this chain and requirements const signedTypedDataForChain = signedTypedData.find((signedTypedData) => isNativePermitValid(signedTypedData, nativePermitData)); if (!signedTypedDataForChain) { statusManager.updateProcess(step, sharedProcess.type, 'ACTION_REQUIRED'); if (!allowUserInteraction) { return { status: 'ACTION_REQUIRED' }; } // Sign the permit const signature = await getAction(updatedClient, signTypedData, 'signTypedData')({ account: updatedClient.account, domain: nativePermitData.domain, types: nativePermitData.types, primaryType: nativePermitData.primaryType, message: nativePermitData.message, }); // Add the new permit to the signed permits array const signedPermit = { ...nativePermitData, signature, }; signedTypedData.push(signedPermit); } statusManager.updateProcess(step, sharedProcess.type, 'DONE', { signedTypedData, }); return { status: 'NATIVE_PERMIT', data: signedTypedData, }; } // Clear the txHash and txLink from potential previous approval transaction statusManager.updateProcess(step, sharedProcess.type, 'ACTION_REQUIRED', { txHash: undefined, txLink: undefined, }); if (!allowUserInteraction) { return { status: 'ACTION_REQUIRED' }; } // Set new allowance const approveAmount = permit2Supported ? MaxUint256 : fromAmount; const approveTxHash = await setAllowance(updatedClient, step.action.fromToken.address, spenderAddress, approveAmount, executionOptions, // We need to return the populated transaction is batching is supported // instead of executing transaction on-chain batchingSupported); // If batching is supported, we need to return the batch approval data // because allowance was't set by standard approval transaction if (batchingSupported) { statusManager.updateProcess(step, sharedProcess.type, 'DONE'); return { status: 'BATCH_APPROVAL', data: { call: { to: step.action.fromToken.address, data: approveTxHash, chainId: step.action.fromToken.chainId, }, signedTypedData, }, }; } await waitForApprovalTransaction(updatedClient, approveTxHash, sharedProcess.type, step, chain, statusManager); return { status: 'DONE', data: signedTypedData }; } catch (e) { if (!sharedProcess) { sharedProcess = statusManager.findOrCreateProcess({ step, type: 'TOKEN_ALLOWANCE', chainId: step.action.fromChainId, }); } const error = await parseEVMErrors(e, step, sharedProcess); statusManager.updateProcess(step, sharedProcess.type, 'FAILED', { error: { message: error.cause.message, code: error.code, }, }); statusManager.updateExecution(step, 'FAILED'); throw error; } }; const waitForApprovalTransaction = async (client, txHash, processType, step, chain, statusManager) => { const baseExplorerUrl = chain.metamask.blockExplorerUrls[0]; const getTxLink = (hash) => `${baseExplorerUrl}tx/${hash}`; statusManager.updateProcess(step, processType, 'PENDING', { txHash, txLink: getTxLink(txHash), }); const transactionReceipt = await waitForTransactionReceipt({ client, chainId: chain.id, txHash, onReplaced(response) { const newHash = response.transaction.hash; statusManager.updateProcess(step, processType, 'PENDING', { txHash: newHash, txLink: getTxLink(newHash), }); }, }); const finalHash = transactionReceipt?.transactionHash || txHash; statusManager.updateProcess(step, processType, 'DONE', { txHash: finalHash, txLink: getTxLink(finalHash), }); }; //# sourceMappingURL=checkAllowance.js.map