@0xsequence/anypay-sdk
Version:
SDK for Anypay functionality
1,126 lines • 63.7 kB
JavaScript
import { useMutation, useQuery } from "@tanstack/react-query";
import { Address } from "ox";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { createPublicClient, createWalletClient, custom, formatUnits, http, isAddressEqual, parseUnits, zeroAddress, } from "viem";
import * as chains from "viem/chains";
import { useEstimateGas, useSendTransaction, useSwitchChain, useWaitForTransactionReceipt, } from "wagmi";
import { useAPIClient } from "./apiClient.js";
import { getERC20TransferData } from "./encoders.js";
import { calculateIntentAddress, commitIntentConfig, getIntentCallsPayloads as getIntentCallsPayloadsFromIntents, sendOriginTransaction, } from "./intents.js";
import { getMetaTxStatus, useMetaTxnsMonitor, } from "./metaTxnMonitor.js";
import { relayerSendMetaTx } from "./metaTxns.js";
import { findFirstPreconditionForChainId, findPreconditionAddress, } from "./preconditions.js";
import { getBackupRelayer, useRelayers } from "./relayer.js";
import { getChainInfo } from "./tokenBalances.js";
import { requestWithTimeout } from "./utils.js";
const RETRY_WINDOW_MS = 10_000;
export function useAnyPay(config) {
const { account, disableAutoExecute = false, env, useV3Relayers = true, sequenceApiKey, } = config;
const apiClient = useAPIClient({ projectAccessKey: sequenceApiKey });
const [isAutoExecute, setIsAutoExecute] = useState(!disableAutoExecute);
const [hasAutoExecuted, setHasAutoExecuted] = useState(false);
// Track timestamps of when each meta-transaction was last sent
const [sentMetaTxns, setSentMetaTxns] = useState({});
// State declarations
const [metaTxns, setMetaTxns] = useState(null);
const [intentCallsPayloads, setIntentCallsPayloads] = useState(null);
const [intentPreconditions, setIntentPreconditions] = useState(null);
const [lifiInfos, setLifiInfos] = useState(null);
const [anypayFee, setAnypayFee] = useState(null);
const [txnHash, setTxnHash] = useState();
const [committedIntentAddress, setCommittedIntentAddress] = useState(null);
// const [preconditionStatuses, setPreconditionStatuses] = useState<boolean[]>([])
const [originCallParams, setOriginCallParams] = useState(null);
const [operationHashes, setOperationHashes] = useState({});
const [isTransactionInProgress, setIsTransactionInProgress] = useState(false);
const [isChainSwitchRequired, setIsChainSwitchRequired] = useState(false);
const { switchChain, isPending: isSwitchingChain, error: switchChainError, } = useSwitchChain();
const { sendTransaction, isPending: isSendingTransaction } = useSendTransaction();
const [isEstimatingGas, setIsEstimatingGas] = useState(false);
const [originCallStatus, setOriginCallStatus] = useState(null);
const [originBlockTimestamp, setOriginBlockTimestamp] = useState(null);
const [metaTxnBlockTimestamps, setMetaTxnBlockTimestamps] = useState({});
const [verificationStatus, setVerificationStatus] = useState(null);
const { getRelayer } = useRelayers({
env,
useV3Relayers,
});
// Add gas estimation hook with proper types
const { data: estimatedGas, isError: isEstimateError, error: estimateError, } = useEstimateGas(originCallParams?.to && originCallParams?.chainId && !originCallParams.error
? {
to: originCallParams.to || undefined,
data: originCallParams.data || undefined,
value: originCallParams.value || undefined,
chainId: originCallParams.chainId || undefined,
}
: undefined);
const commitIntentConfigMutation = useMutation({
mutationFn: async (args) => {
if (!apiClient)
throw new Error("API client not available");
if (!args.lifiInfos)
throw new Error("LifiInfos not available");
try {
console.log("Calculating intent address...");
console.log("Main signer:", args.mainSigner);
console.log("Calls:", args.calls);
console.log("LifiInfos:", args.lifiInfos);
const calculatedAddress = calculateIntentAddress(args.mainSigner, args.calls, // TODO: Add proper type
args.lifiInfos);
const receivedAddress = findPreconditionAddress(args.preconditions);
console.log("Calculated address:", calculatedAddress.toString());
console.log("Received address:", receivedAddress);
const isVerified = isAddressEqual(Address.from(receivedAddress), calculatedAddress);
setVerificationStatus({
success: isVerified,
receivedAddress: receivedAddress,
calculatedAddress: calculatedAddress.toString(),
});
if (!isVerified) {
throw new Error("Address verification failed: Calculated address does not match received address.");
}
// Commit the intent config
const response = await apiClient.commitIntentConfig({
walletAddress: calculatedAddress.toString(),
mainSigner: args.mainSigner,
calls: args.calls,
preconditions: args.preconditions,
lifiInfos: args.lifiInfos,
});
console.log("API Commit Response:", response);
return { calculatedAddress: calculatedAddress.toString(), response };
}
catch (error) {
console.error("Error during commit intent mutation:", error);
if (!verificationStatus?.success &&
!verificationStatus?.receivedAddress) {
try {
const calculatedAddress = calculateIntentAddress(args.mainSigner, args.calls, // TODO: Add proper type
args.lifiInfos);
const receivedAddress = findPreconditionAddress(args.preconditions);
setVerificationStatus({
success: false,
receivedAddress: receivedAddress,
calculatedAddress: calculatedAddress.toString(),
});
}
catch (calcError) {
console.error("Error calculating addresses for verification status on failure:", calcError);
setVerificationStatus({ success: false });
}
}
throw error;
}
},
onSuccess: (data) => {
console.log("Intent config committed successfully, Wallet Address:", data.calculatedAddress);
setCommittedIntentAddress(data.calculatedAddress);
},
onError: (error) => {
console.error("Failed to commit intent config:", error);
setCommittedIntentAddress(null);
},
});
// New Query to fetch committed intent config
const { data: committedIntentConfig, isLoading: isLoadingCommittedConfig, error: committedConfigError, } = useQuery({
queryKey: ["getIntentConfig", committedIntentAddress],
queryFn: async () => {
if (!apiClient || !committedIntentAddress) {
throw new Error("API client or committed intent address not available");
}
console.log("Fetching intent config for address:", committedIntentAddress);
return await apiClient.getIntentConfig({
walletAddress: committedIntentAddress,
});
},
enabled: !!committedIntentAddress &&
!!apiClient &&
commitIntentConfigMutation.isSuccess,
staleTime: 1000 * 60 * 5, // 5 minutes
retry: 1,
});
async function getIntentCallsPayloads(args) {
return getIntentCallsPayloadsFromIntents(apiClient, args);
}
// TODO: Add type for args
const createIntentMutation = useMutation({
mutationFn: async (args) => {
if (args.originChainId === args.destinationChainId &&
isAddressEqual(Address.from(args.originTokenAddress), Address.from(args.destinationTokenAddress))) {
throw new Error("The same token cannot be used as both the source and destination token.");
}
if (!account.address) {
throw new Error("Missing selected token or account address");
}
// Reset commit state when generating a new intent
setCommittedIntentAddress(null);
setVerificationStatus(null);
setAnypayFee(null);
setMetaTxns(null);
setIntentCallsPayloads(null);
setIntentPreconditions(null);
setLifiInfos(null);
const data = await getIntentCallsPayloads(args);
setMetaTxns(data.metaTxns);
setIntentCallsPayloads(data.calls);
setIntentPreconditions(data.preconditions);
setLifiInfos(data.lifiInfos);
setAnypayFee(data.anypayFee);
setCommittedIntentAddress(null);
setVerificationStatus(null);
return data;
},
onSuccess: (data) => {
console.log("Intent Config Success:", data);
setAnypayFee(data.anypayFee || null);
setLifiInfos(data.lifiInfos || null);
if (data?.calls &&
data.calls.length > 0 &&
data.preconditions &&
data.preconditions.length > 0 &&
data.metaTxns &&
data.metaTxns.length > 0) {
setIntentCallsPayloads(data.calls);
setIntentPreconditions(data.preconditions);
setMetaTxns(data.metaTxns);
}
else {
console.warn("API returned success but no operations found.");
setIntentCallsPayloads(null);
setIntentPreconditions(null);
setMetaTxns(null);
}
},
onError: (error) => {
console.error("Intent Config Error:", error);
setIntentCallsPayloads(null);
setIntentPreconditions(null);
setMetaTxns(null);
setLifiInfos(null);
setAnypayFee(null);
},
});
function callIntentCallsPayload(args) {
createIntentMutation.mutate(args);
}
const clearIntent = useCallback(() => {
console.log("[AnyPay] Clearing intent state");
setIntentCallsPayloads(null);
setIntentPreconditions(null);
setMetaTxns(null);
setLifiInfos(null);
setAnypayFee(null);
setCommittedIntentAddress(null);
setVerificationStatus(null);
setOperationHashes({});
setHasAutoExecuted(false);
setMetaTxnBlockTimestamps({});
}, []); // Empty deps array since these setters are stable
const updateOriginCallStatus = useCallback((hash, status, gasUsed, effectiveGasPrice, revertReason) => {
setOriginCallStatus({
txnHash: hash,
status: status === "success"
? "Success"
: status === "reverted"
? "Failed"
: status === "sending"
? "Sending..."
: "Pending",
revertReason: status === "reverted"
? revertReason || "Transaction reverted"
: undefined,
gasUsed: gasUsed ? Number(gasUsed) : undefined,
effectiveGasPrice: effectiveGasPrice?.toString(),
});
}, []);
const sendOriginTransaction = async () => {
console.log("Sending origin transaction...");
console.log(isTransactionInProgress, originCallParams, originCallParams?.error, originCallParams?.to, originCallParams?.data, originCallParams?.value, originCallParams?.chainId);
if (isTransactionInProgress || // Prevent duplicate transactions
!originCallParams ||
originCallParams.error ||
!originCallParams.to ||
originCallParams.data === null ||
originCallParams.value === null ||
originCallParams.chainId === null) {
console.error("Origin call parameters not available or invalid:", originCallParams);
updateOriginCallStatus(undefined, "reverted", undefined, undefined, "Origin call parameters not ready");
return;
}
// Check if we need to switch chains
if (account.chainId !== originCallParams.chainId) {
setIsChainSwitchRequired(true);
updateOriginCallStatus(undefined, "pending", undefined, undefined, `Switching to chain ${originCallParams.chainId}...`);
const walletClient = createWalletClient({
chain: getChainInfo(originCallParams.chainId),
transport: custom((await account.connector.getProvider())), // TODO: Add proper type
});
try {
await attemptSwitchChain(walletClient, originCallParams.chainId);
setIsChainSwitchRequired(false);
}
catch (error) {
console.error("Chain switch failed:", error);
if (error instanceof Error && error.message.includes("User rejected")) {
setIsAutoExecute(false);
}
updateOriginCallStatus(undefined, "reverted", undefined, undefined, error instanceof Error
? error.message
: "Unknown error switching chain");
setIsChainSwitchRequired(false);
return; // Stop execution on switch failure
}
}
// Ensure only one transaction is sent at a time
if (!isTransactionInProgress) {
setIsTransactionInProgress(true); // Mark transaction as in progress
setTxnHash(undefined);
updateOriginCallStatus(undefined, "sending");
if (!estimatedGas && !isEstimateError) {
setIsEstimatingGas(true);
return; // Wait for gas estimation
}
if (isEstimateError) {
console.error("Gas estimation failed:", estimateError);
updateOriginCallStatus(undefined, "reverted", undefined, undefined, `Gas estimation failed: ${estimateError?.message}`);
setIsTransactionInProgress(false);
return;
}
// Add 20% buffer to estimated gas
const gasLimit = estimatedGas
? BigInt(Math.floor(Number(estimatedGas) * 1.2))
: undefined;
sendTransaction({
to: originCallParams.to,
data: originCallParams.data,
value: originCallParams.value,
chainId: originCallParams.chainId,
gas: gasLimit,
}, {
onSuccess: (hash) => {
console.log("Transaction sent, hash:", hash);
setTxnHash(hash);
setIsTransactionInProgress(false); // Reset transaction state
},
onError: (error) => {
console.error("Transaction failed:", error);
if (error instanceof Error &&
(error.message.includes("User rejected") ||
error.message.includes("user rejected"))) {
setIsAutoExecute(false);
}
updateOriginCallStatus(undefined, "reverted", undefined, undefined, error instanceof Error ? error.message : "Unknown error");
setIsTransactionInProgress(false);
},
});
}
else {
console.warn("Transaction already in progress. Skipping duplicate request.");
}
};
// Remove the chain change effect that might be resetting state
useEffect(() => {
if (switchChainError) {
console.error("Chain switch error:", switchChainError);
updateOriginCallStatus(undefined, "reverted", undefined, undefined, `Chain switch failed: ${switchChainError.message || "Unknown error"}`);
setIsChainSwitchRequired(false);
}
}, [switchChainError, updateOriginCallStatus]);
// Reset gas estimation state when parameters change
useEffect(() => {
setIsEstimatingGas(false);
}, []);
// Only update chain switch required state when needed
useEffect(() => {
if (originCallParams?.chainId &&
account.chainId === originCallParams.chainId) {
console.log("No chain switch required");
setIsChainSwitchRequired(false);
}
}, [account.chainId, originCallParams?.chainId]);
// Effect to handle chain switching
useEffect(() => {
if (originCallParams?.chainId &&
account.chainId !== originCallParams.chainId) {
async function check() {
try {
const chainId = originCallParams.chainId;
const walletClient = createWalletClient({
chain: getChainInfo(chainId),
transport: custom((await account.connector.getProvider())), // TODO: Add proper type
});
await attemptSwitchChain(walletClient, chainId);
}
catch (error) {
console.error("Chain switch failed:", error);
}
}
check().catch(console.error);
}
}, [account, originCallParams]);
// Hook to wait for transaction receipt
const { data: receipt, isLoading: isWaitingForReceipt, isSuccess: receiptIsSuccess, isError: receiptIsError, error: receiptError, } = useWaitForTransactionReceipt({
hash: txnHash,
confirmations: 1,
query: {
enabled: !!txnHash,
},
});
// Modify the effect that watches for transaction status
useEffect(() => {
if (!txnHash) {
// Only reset these when txnHash is cleared
if (originCallStatus?.txnHash) {
setOriginCallStatus(null);
}
setOriginBlockTimestamp(null);
if (Object.keys(sentMetaTxns).length > 0) {
setSentMetaTxns({});
}
return;
}
if (originCallStatus?.txnHash === txnHash &&
(originCallStatus?.status === "Success" ||
originCallStatus?.status === "Failed") &&
!isWaitingForReceipt) {
return;
}
if (isWaitingForReceipt) {
setOriginCallStatus((prevStatus) => ({
...(prevStatus?.txnHash === txnHash
? prevStatus
: {
gasUsed: undefined,
effectiveGasPrice: undefined,
revertReason: undefined,
}),
txnHash,
status: "Pending",
}));
return;
}
if (receiptIsSuccess && receipt) {
const newStatus = receipt.status === "success" ? "Success" : "Failed";
setOriginCallStatus({
txnHash: receipt.transactionHash,
status: newStatus,
gasUsed: receipt.gasUsed ? Number(receipt.gasUsed) : undefined,
effectiveGasPrice: receipt.effectiveGasPrice?.toString(),
revertReason: receipt.status === "reverted"
? receiptError?.message ||
"Transaction reverted by receipt"
: undefined,
});
if (newStatus === "Success" && receipt.blockNumber) {
const fetchTimestamp = async () => {
try {
if (!originCallParams?.chainId) {
console.error("[AnyPay] Origin chainId not available for fetching origin block timestamp.");
setOriginBlockTimestamp(null);
return;
}
const chainConfig = getChainInfo(originCallParams.chainId);
const client = createPublicClient({
chain: chainConfig,
transport: http(),
});
const block = await client.getBlock({
blockNumber: BigInt(receipt.blockNumber),
});
setOriginBlockTimestamp(Number(block.timestamp));
}
catch (error) {
console.error("[AnyPay] Error fetching origin block timestamp:", error);
setOriginBlockTimestamp(null);
}
};
fetchTimestamp();
}
else if (newStatus !== "Success") {
setOriginBlockTimestamp(null);
}
if (newStatus === "Success" &&
metaTxns &&
metaTxns.length > 0 &&
isAutoExecute &&
!metaTxns.some((tx) => sentMetaTxns[`${tx.chainId}-${tx.id}`])) {
console.log("Origin transaction successful, auto-sending all meta transactions...");
sendMetaTxnMutation.mutate({ selectedId: null });
}
}
else if (receiptIsError) {
setOriginCallStatus({
txnHash,
status: "Failed",
revertReason: receiptError?.message ||
"Failed to get receipt",
gasUsed: undefined,
effectiveGasPrice: undefined,
});
setOriginBlockTimestamp(null);
}
}, [
txnHash,
isWaitingForReceipt,
receiptIsSuccess,
receiptIsError,
receipt,
receiptError,
metaTxns,
sentMetaTxns,
isAutoExecute,
originCallParams?.chainId,
originCallStatus?.status,
originCallStatus?.txnHash,
]);
// Modify the auto-execute effect
useEffect(() => {
const shouldAutoSend = isAutoExecute &&
commitIntentConfigMutation.isSuccess &&
originCallParams?.chainId &&
account.chainId === originCallParams.chainId &&
!originCallParams.error &&
originCallParams.to &&
originCallParams.data !== null &&
originCallParams.value !== null &&
!isSendingTransaction &&
!isWaitingForReceipt &&
!txnHash &&
!isChainSwitchRequired &&
!originCallStatus &&
!hasAutoExecuted;
if (shouldAutoSend) {
console.log("Auto-executing transaction: All conditions met.");
setHasAutoExecuted(true);
// Set initial status
setOriginCallStatus({
status: "Sending...",
});
sendTransaction({
to: originCallParams.to,
data: originCallParams.data,
value: originCallParams.value,
chainId: originCallParams.chainId,
}, {
onSuccess: (hash) => {
console.log("Auto-executed transaction sent, hash:", hash);
setTxnHash(hash);
},
onError: (error) => {
console.error("Auto-executed transaction failed:", error);
if (error instanceof Error &&
(error.message.includes("User rejected") ||
error.message.includes("user rejected"))) {
setIsAutoExecute(false);
}
setOriginCallStatus({
status: "Failed",
revertReason: error instanceof Error ? error.message : "Unknown error",
});
setHasAutoExecuted(false);
},
});
}
}, [
isAutoExecute,
commitIntentConfigMutation.isSuccess,
originCallParams,
account.chainId,
isSendingTransaction,
isWaitingForReceipt,
txnHash,
isChainSwitchRequired,
originCallStatus,
hasAutoExecuted,
sendTransaction,
]);
// Effect to auto-commit when intent calls payloads are ready
useEffect(() => {
if (isAutoExecute &&
intentCallsPayloads &&
intentPreconditions &&
lifiInfos &&
account.address &&
calculatedIntentAddress &&
!commitIntentConfigMutation.isPending &&
!commitIntentConfigMutation.isSuccess) {
console.log("Auto-committing intent configuration...");
commitIntentConfigMutation.mutate({
walletAddress: calculatedIntentAddress.toString(),
mainSigner: account.address,
calls: intentCallsPayloads,
preconditions: intentPreconditions,
lifiInfos: lifiInfos,
});
}
}, [
isAutoExecute,
intentCallsPayloads,
intentPreconditions,
lifiInfos, // Add lifiInfos dependency
account.address,
commitIntentConfigMutation,
commitIntentConfigMutation.isPending,
commitIntentConfigMutation.isSuccess,
]);
// Update the sendMetaTxn mutation
const sendMetaTxnMutation = useMutation({
mutationFn: async ({ selectedId }) => {
if (!intentCallsPayloads ||
!intentPreconditions ||
!metaTxns ||
!account.address ||
!lifiInfos) {
throw new Error("Missing required data for meta-transaction");
}
const intentAddress = calculateIntentAddress(account.address, intentCallsPayloads, lifiInfos); // TODO: Add proper type
// If no specific ID is selected, send all meta transactions
const txnsToSend = selectedId
? [metaTxns.find((tx) => tx.id === selectedId)]
: metaTxns;
if (!txnsToSend || (selectedId && !txnsToSend[0])) {
throw new Error("Meta transaction not found");
}
const results = [];
for (const metaTxn of txnsToSend) {
if (!metaTxn)
continue;
const operationKey = `${metaTxn.chainId}-${metaTxn.id}`;
const lastSentTime = sentMetaTxns[operationKey];
const now = Date.now();
if (lastSentTime && now - lastSentTime < RETRY_WINDOW_MS) {
const timeLeft = Math.ceil((RETRY_WINDOW_MS - (now - lastSentTime)) / 1000);
console.log(`Meta transaction for ${operationKey} was sent recently. Wait ${timeLeft}s before retry`);
continue;
}
try {
const chainId = parseInt(metaTxn.chainId);
if (Number.isNaN(chainId) || chainId <= 0) {
throw new Error(`Invalid chainId for meta transaction: ${chainId}`);
}
const chainRelayer = getRelayer(chainId);
if (!chainRelayer) {
throw new Error(`No relayer found for chainId: ${chainId}`);
}
const relevantPreconditions = intentPreconditions.filter((p) => p.chainId && parseInt(p.chainId) === chainId);
console.log(`Relaying meta transaction ${operationKey} to intent ${intentAddress} via relayer:`, chainRelayer);
const { opHash } = await chainRelayer.sendMetaTxn(metaTxn.walletAddress, metaTxn.contract, metaTxn.input, BigInt(metaTxn.chainId), undefined, relevantPreconditions);
const useBackupRelayer = false; // Disable backup relayer for now
if (useBackupRelayer) {
try {
// Fire and forget send tx to backup relayer
const backupRelayer = getBackupRelayer(chainId);
backupRelayer
?.sendMetaTxn(metaTxn.walletAddress, metaTxn.contract, metaTxn.input, BigInt(metaTxn.chainId), undefined, relevantPreconditions)
.then(() => { })
.catch(() => { });
}
catch { }
}
results.push({
operationKey,
opHash,
success: true,
});
}
catch (error) {
results.push({
operationKey,
error: error instanceof Error ? error.message : "Unknown error",
success: false,
});
}
}
return results;
},
onSuccess: (results) => {
// Update states based on results
results.forEach(({ operationKey, opHash, success }) => {
if (success && opHash) {
setSentMetaTxns((prev) => ({
...prev,
[operationKey]: Date.now(),
}));
setOperationHashes((prev) => ({
...prev,
[operationKey]: opHash,
}));
}
});
},
onError: (error) => {
console.error("Error in meta-transaction process:", error);
},
retry: 5, // Allow up to 2 retries
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), // Exponential backoff
});
const [tokenAddress, setTokenAddress] = useState(null);
const [originChainId, setOriginChainId] = useState(null);
useEffect(() => {
if (!intentCallsPayloads?.[0]?.chainId ||
!tokenAddress ||
!originChainId ||
!intentPreconditions ||
!account.address) {
setOriginCallParams(null);
return;
}
try {
const intentAddressString = calculatedIntentAddress;
let calcTo;
let calcData = "0x";
let calcValue = 0n;
const recipientAddress = intentAddressString;
const isNative = tokenAddress === zeroAddress;
if (isNative) {
const nativePrecondition = intentPreconditions.find((p) => (p.type === "transfer-native" || p.type === "native-balance") &&
p.chainId === originChainId.toString());
const nativeMinAmount = nativePrecondition?.data?.minAmount?.toString() ??
nativePrecondition?.data?.min?.toString();
if (nativeMinAmount === undefined) {
throw new Error("Could not find native precondition (transfer-native or native-balance) or min amount");
}
calcValue = BigInt(nativeMinAmount);
calcTo = recipientAddress;
}
else {
const erc20Precondition = intentPreconditions.find((p) => p.type === "erc20-balance" &&
p.chainId === originChainId.toString() &&
p.data?.token &&
isAddressEqual(Address.from(p.data.token), Address.from(tokenAddress)));
const erc20MinAmount = erc20Precondition?.data?.min?.toString();
if (erc20MinAmount === undefined) {
throw new Error("Could not find ERC20 balance precondition or min amount");
}
calcData = getERC20TransferData(recipientAddress, erc20MinAmount);
calcTo = tokenAddress;
}
setOriginCallParams({
to: calcTo,
data: calcData,
value: calcValue,
chainId: originChainId,
error: undefined,
});
}
catch (error) {
console.error("Failed to calculate origin call params for UI:", error);
setOriginCallParams({
to: null,
data: null,
value: null,
chainId: null,
error: error instanceof Error ? error.message : "Unknown error",
});
}
}, [
intentCallsPayloads,
tokenAddress,
originChainId,
intentPreconditions,
account.address,
]);
// const checkPreconditionStatuses = useCallback(async () => {
// if (!intentPreconditions) return
// const statuses = await Promise.all(
// intentPreconditions.map(async (precondition) => {
// try {
// const chainIdString = precondition.chainId
// if (!chainIdString) {
// console.warn('Precondition missing chainId:', precondition)
// return false
// }
// const chainId = parseInt(chainIdString)
// if (isNaN(chainId) || chainId <= 0) {
// console.warn('Precondition has invalid chainId:', chainIdString, precondition)
// return false
// }
// const chainRelayer = getRelayer(chainId)
// if (!chainRelayer) {
// console.error(`No relayer found for chainId: ${chainId}`)
// return false
// }
// return await chainRelayer.checkPrecondition(precondition)
// } catch (error) {
// console.error('Error checking precondition:', error, 'Precondition:', precondition)
// return false
// }
// }),
// )
// setPreconditionStatuses(statuses)
// }, [intentPreconditions, getRelayer])
// useEffect(() => {
// // TODO: Remove this once we have a way to check precondition statuses
// if (false) {
// checkPreconditionStatuses()
// }
// }, [intentPreconditions, checkPreconditionStatuses])
// Add monitoring for each meta transaction
const metaTxnMonitorStatuses = useMetaTxnsMonitor(metaTxns, getRelayer);
// Create a stable dependency for the meta timestamp effect
const _stableMetaTxnStatusesKey = useMemo(() => {
if (!metaTxns || Object.keys(metaTxnMonitorStatuses).length === 0) {
return "no_statuses";
}
// Sort by a stable key (e.g., id) to ensure consistent order if metaTxns array order changes
// but content is the same, though metaTxns itself is a dependency, so this might be redundant if metaTxns order is stable.
const sortedTxnIds = metaTxns
.map((tx) => `${tx.chainId}-${tx.id}`)
.sort();
return sortedTxnIds
.map((key) => {
const statusObj = metaTxnMonitorStatuses[key];
return `${key}:${statusObj ? statusObj.status : "loading"}`;
})
.join(",");
}, [metaTxns, metaTxnMonitorStatuses]);
const processedTxns = useRef(new Set());
// Effect to fetch meta-transaction block timestamps
useEffect(() => {
console.log("[AnyPay] Running meta-transaction block timestamp effect:", {
metaTxnsLength: metaTxns?.length,
monitorStatusesLength: Object.keys(metaTxnMonitorStatuses).length,
});
if (!metaTxns || metaTxns.length === 0) {
console.log("[AnyPay] No meta transactions, clearing timestamps");
processedTxns.current.clear();
if (Object.keys(metaTxnBlockTimestamps).length > 0) {
setMetaTxnBlockTimestamps({});
}
return;
}
if (!Object.keys(metaTxnMonitorStatuses).length) {
console.log("[AnyPay] No monitor statuses yet, waiting...");
return;
}
metaTxns.forEach(async (metaTxn) => {
const operationKey = `${metaTxn.chainId}-${metaTxn.id}`;
// Skip if already processed
if (processedTxns.current.has(operationKey)) {
console.log(`[AnyPay] MetaTxn ${operationKey}: Already processed, skipping`);
return;
}
const monitorStatus = metaTxnMonitorStatuses[operationKey];
if (!monitorStatus || monitorStatus.status !== "confirmed") {
console.log(`[AnyPay] MetaTxn ${operationKey}: Status not confirmed, skipping`);
return;
}
// Type assertion since we know it exists when status is "confirmed"
const transactionHash = monitorStatus.transactionHash;
if (!transactionHash) {
console.log(`[AnyPay] MetaTxn ${operationKey}: No transaction hash, skipping`);
return;
}
console.log(`[AnyPay] MetaTxn ${operationKey}: Processing transaction ${transactionHash}`);
processedTxns.current.add(operationKey);
try {
const chainId = parseInt(metaTxn.chainId);
if (Number.isNaN(chainId) || chainId <= 0) {
throw new Error(`Invalid chainId for meta transaction: ${metaTxn.chainId}`);
}
const chainConfig = getChainInfo(chainId);
const client = createPublicClient({
chain: chainConfig,
transport: http(),
});
const receipt = await client.getTransactionReceipt({
hash: transactionHash,
});
if (receipt && typeof receipt.blockNumber === "bigint") {
const block = await client.getBlock({
blockNumber: receipt.blockNumber,
});
console.log(`[AnyPay] MetaTxn ${operationKey}: Got block timestamp ${block.timestamp}`);
setMetaTxnBlockTimestamps((prev) => ({
...prev,
[operationKey]: {
timestamp: Number(block.timestamp),
error: undefined,
},
}));
}
else {
console.warn(`[AnyPay] MetaTxn ${operationKey}: No block number in receipt`);
setMetaTxnBlockTimestamps((prev) => ({
...prev,
[operationKey]: {
timestamp: null,
error: "Block number not found in receipt",
},
}));
}
}
catch (error) {
console.error(`[AnyPay] MetaTxn ${operationKey}: Error:`, error);
setMetaTxnBlockTimestamps((prev) => ({
...prev,
[operationKey]: {
timestamp: null,
error: error.message || "Failed to fetch receipt/timestamp",
},
}));
}
});
}, [metaTxns, metaTxnMonitorStatuses, metaTxnBlockTimestamps]);
const updateAutoExecute = (enabled) => {
setIsAutoExecute(enabled);
};
function createIntent(args) {
createIntentMutation.mutate(args);
}
const calculatedIntentAddress = useMemo(() => {
if (!account.address || !intentCallsPayloads || !lifiInfos) {
return null;
}
return calculateIntentAddress(account.address, intentCallsPayloads, lifiInfos); // TODO: Add proper type
}, [account.address, intentCallsPayloads, lifiInfos]);
const createIntentPending = createIntentMutation.isPending;
const createIntentSuccess = createIntentMutation.isSuccess;
const createIntentError = createIntentMutation.error;
const createIntentArgs = createIntentMutation.variables;
function commitIntentConfig(args) {
console.log("commitIntentConfig", args);
commitIntentConfigMutation.mutate(args);
}
function updateOriginCallParams(args) {
if (!args) {
setOriginCallParams(null);
return;
}
const { originChainId, tokenAddress } = args;
setOriginChainId(originChainId);
setTokenAddress(tokenAddress);
}
function sendMetaTxn(selectedId) {
sendMetaTxnMutation.mutate({ selectedId });
}
const commitIntentConfigPending = commitIntentConfigMutation.isPending;
const commitIntentConfigSuccess = commitIntentConfigMutation.isSuccess;
const commitIntentConfigError = commitIntentConfigMutation.error;
const commitIntentConfigArgs = commitIntentConfigMutation.variables;
const sendMetaTxnPending = sendMetaTxnMutation.isPending;
const sendMetaTxnSuccess = sendMetaTxnMutation.isSuccess;
const sendMetaTxnError = sendMetaTxnMutation.error;
const sendMetaTxnArgs = sendMetaTxnMutation.variables;
return {
apiClient,
metaTxns,
intentCallsPayloads,
intentPreconditions,
lifiInfos,
anypayFee,
txnHash,
committedIntentAddress,
verificationStatus,
getRelayer,
estimatedGas,
isEstimateError,
estimateError,
calculateIntentAddress,
committedIntentConfig,
isLoadingCommittedConfig,
committedConfigError,
commitIntentConfig,
commitIntentConfigPending,
commitIntentConfigSuccess,
commitIntentConfigError,
commitIntentConfigArgs,
getIntentCallsPayloads,
operationHashes,
callIntentCallsPayload,
sendOriginTransaction,
switchChain,
isSwitchingChain,
switchChainError,
isTransactionInProgress,
isChainSwitchRequired,
sendTransaction,
isSendingTransaction,
originCallStatus,
updateOriginCallStatus,
isEstimatingGas,
isAutoExecute,
updateAutoExecute,
receipt,
isWaitingForReceipt,
receiptIsSuccess,
receiptIsError,
receiptError,
hasAutoExecuted,
sentMetaTxns,
sendMetaTxn,
sendMetaTxnPending,
sendMetaTxnSuccess,
sendMetaTxnError,
sendMetaTxnArgs,
clearIntent,
metaTxnMonitorStatuses,
createIntent,
createIntentPending,
createIntentSuccess,
createIntentError,
createIntentArgs,
calculatedIntentAddress,
originCallParams,
updateOriginCallParams,
originBlockTimestamp,
metaTxnBlockTimestamps,
};
}
// TODO: fix up this one-click send
export async function prepareSend(options) {
const { account, originTokenAddress, originChainId, originTokenAmount, // account balance
destinationChainId, recipient, destinationTokenAddress, destinationTokenAmount, destinationTokenSymbol, fee, client: walletClient, dryMode, apiClient, originRelayer, destinationRelayer, destinationCalldata, onTransactionStateChange, sourceTokenPriceUsd, destinationTokenPriceUsd, sourceTokenDecimals, destinationTokenDecimals, } = options;
if (!walletClient) {
throw new Error("Wallet client not provided");
}
const chain = getChainInfo(originChainId);
const isToSameChain = originChainId === destinationChainId;
const isToSameToken = originTokenAddress === destinationTokenAddress;
const publicClient = createPublicClient({
chain,
transport: http(),
});
const mainSigner = account.address;
const _destinationCalldata = destinationCalldata ||
(destinationTokenAddress === zeroAddress
? "0x"
: getERC20TransferData(recipient, BigInt(destinationTokenAmount)));
const _destinationToAddress = destinationCalldata
? recipient
: destinationTokenAddress === zeroAddress
? recipient
: destinationTokenAddress;
const _destinationCallValue = destinationTokenAddress === zeroAddress ? destinationTokenAmount : "0";
const intentArgs = {
userAddress: mainSigner,
originChainId,
originTokenAddress,
originTokenAmount: originTokenAddress === destinationTokenAddress
? destinationTokenAmount
: originTokenAmount, // max amount
destinationChainId,
destinationToAddress: _destinationToAddress,
destinationTokenAddress: destinationTokenAddress,
destinationTokenAmount: destinationTokenAmount,
destinationTokenSymbol: destinationTokenSymbol,
destinationCallData: _destinationCalldata,
destinationCallValue: _destinationCallValue,
};
const transactionStates = [];
// origin tx
transactionStates.push({
transactionHash: "",
explorerUrl: "",
chainId: originChainId,
state: "pending",
});
if (!isToSameChain) {
// swap + bridge tx
transactionStates.push({
transactionHash: "",
explorerUrl: "",
chainId: originChainId,
state: "pending",
});
// destination tx
transactionStates.push({
transactionHash: "",
explorerUrl: "",
chainId: destinationChainId,
state: "pending",
});
}
if (isToSameChain && !isToSameToken) {
// swap tx
transactionStates.push({
transactionHash: "",
explorerUrl: "",
chainId: originChainId,
state: "pending",
});
}
if (isToSameToken && isToSameChain) {
return {
send: async (onOriginSend) => {
const originCallParams = {
to: destinationCalldata
? recipient
: originTokenAddress === zeroAddress
? recipient
: originTokenAddress,
data: destinationCalldata ||
(originTokenAddress === zeroAddress
? "0x"
: getERC20TransferData(recipient, BigInt(destinationTokenAmount))),
value: originTokenAddress === zeroAddress
? BigInt(destinationTokenAmount)
: "0",
chainId: originChainId,
chain,
};
console.log("origin call params", originCallParams);
let originUserTxReceipt = null;
const originMetaTxnReceipt = null; // TODO: Add proper type
const destinationMetaTxnReceipt = null; // TODO: Add proper type
await attemptSwitchChain(walletClient, originChainId);
if (!dryMode) {
onTransactionStateChange([
{
transactionHash: "",
explorerUrl: "",
chainId: originChainId,
state: "pending",
},
]);
console.log("origin call params", originCallParams);
const txHash = await sendOriginTransaction(account, walletClient, originCallParams); // TODO: Add proper type
console.log("origin tx", txHash);
if (onOriginSend) {
onOriginSend();
}
// Wait for transaction receipt
const receipt = await publicClient.waitForTransactionReceipt({
hash: txHash,
});
console.log("receipt", receipt);
originUserTxReceipt = receipt;
onTransactionStateChange([
{
transactionHash: originUserTxReceipt?.transactionHash,
explorerUrl: getExplorerUrl(originUserTxReceipt?.transactionHash, originChainId),
chainId: originChainId,
state: originUserTxReceipt?.status === "success"
? "confirmed"
: "failed",
},
]);
}
return {
originUserTxReceipt,
originMetaTxnReceipt,
destinationMetaTxnReceipt,
};
},
};
}
console.log("Creating intent with args:", intentArgs);
const intent = await getIntentCallsPayloadsFromIntents(apiClient, intentArgs); // TODO: Add proper type
console.log("Got intent:", intent);
if (!intent) {
throw new Error("Invalid intent");
}
if (!intent.preconditions?.length ||
!intent.calls?.length ||
!intent.lifiInfos?.length) {
throw new Error("Invalid intent");
}
const intentAddress = calculateIntentAddress(mainSigner, intent.calls, intent.lifiInfos); // TODO: Add proper type
console.log("Calculated intent address:", intentAddress.toString());
await commitIntentConfig(apiClient, mainSigner, intent.calls, intent.preconditions, intent.lifiInfos);
console.log("Committed intent config");
const firstPrecondition = findFirstPreconditionForChainId(intent.preconditions, originChainId);
if (!firstPrecondition) {
throw new Error("No precondition found for origin chain");
}
const firstPreconditionAddress = firstPreco