@lifi/sdk
Version:
LI.FI Any-to-Any Cross-Chain-Swap SDK
219 lines • 10.5 kB
JavaScript
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