Documentation Index
Fetch the complete documentation index at: https://docs.li.fi/llms.txt
Use this file to discover all available pages before exploring further.
A PARTIAL completion occurs when a cross-chain transfer succeeds but the user receives a different token than requested. This typically happens when the destination swap fails but the bridge succeeds. This page explains how to detect, interpret, and recover from partial completions.
What is a Partial Completion?
In a cross-chain transfer, there are often multiple steps:
Source Chain Destination Chain
[Token A] → [Swap to B] → [Bridge] → [Receive B] → [Swap to C]
If the final swap on the destination chain fails (e.g., due to slippage), the user receives token B instead of token C. The value is preserved, but the token is different.
Example Scenario
- Requested: 10 USDC on Ethereum → ETH on Arbitrum
- Actual Result: 10 USDC on Ethereum → 10 USDC on Arbitrum (swap failed)
- Status:
DONE with substatus PARTIAL
Detecting Partial Completion
Check the status response:
const result = await pollTransferStatus(params);
if (result.partial) {
// User received a different token
const received = result.status.receiving;
const requested = quote.action.toToken;
console.log(`Requested: ${requested.symbol}`);
console.log(`Received: ${received.token.symbol}`);
}
Status Response for PARTIAL
{
"status": "DONE",
"substatus": "PARTIAL",
"sending": {
"chainId": 1,
"amount": "10000000",
"token": {
"symbol": "USDC",
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
}
},
"receiving": {
"chainId": 42161,
"amount": "10000000",
"token": {
"symbol": "USDC",
"address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"
}
},
"tool": "stargateV2"
}
Recovery Flow
When a partial completion occurs, offer the user a same-chain swap to get their intended token.
function extractReceivedToken(status) {
const { receiving } = status;
return {
chainId: receiving.chainId,
address: receiving.token.address,
symbol: receiving.token.symbol,
decimals: receiving.token.decimals,
amount: receiving.amount
};
}
Step 2: Get a Same-Chain Swap Quote
Request a quote to swap the received token to the intended token:
async function getRecoveryQuote(status, originalQuote, userAddress) {
const received = extractReceivedToken(status);
const intended = originalQuote.action.toToken;
// Same-chain swap on the destination chain
const params = {
fromChain: received.chainId,
toChain: received.chainId, // Same chain
fromToken: received.address,
toToken: intended.address,
fromAmount: received.amount,
fromAddress: userAddress
};
const response = await fetch(
`https://li.quest/v1/quote?` + new URLSearchParams(params)
);
return await response.json();
}
Step 3: Present Options to User
async function handlePartialCompletion(status, originalQuote, userAddress) {
const received = extractReceivedToken(status);
const intended = originalQuote.action.toToken;
console.log('\n=== Partial Transfer Detected ===');
console.log(`You received: ${formatAmount(received.amount, received.decimals)} ${received.symbol}`);
console.log(`You requested: ${intended.symbol}`);
// Get recovery quote
try {
const recoveryQuote = await getRecoveryQuote(status, originalQuote, userAddress);
const expectedOutput = formatAmount(
recoveryQuote.estimate.toAmount,
intended.decimals
);
console.log(`\nRecovery option available:`);
console.log(`Swap ${received.symbol} → ${intended.symbol}`);
console.log(`Expected output: ${expectedOutput} ${intended.symbol}`);
return {
hasRecoveryOption: true,
received,
recoveryQuote
};
} catch (error) {
console.log(`\nNo direct swap available for ${received.symbol} → ${intended.symbol}`);
return {
hasRecoveryOption: false,
received,
error: error.message
};
}
}
function formatAmount(amount, decimals) {
return (Number(amount) / Math.pow(10, decimals)).toFixed(4);
}
Complete Recovery Implementation
async function executePartialRecovery(
status,
originalQuote,
walletClient,
publicClient,
userAddress
) {
// 1. Extract what was received
const received = extractReceivedToken(status);
const intended = originalQuote.action.toToken;
console.log('Partial completion detected');
console.log(`Received: ${received.amount} ${received.symbol}`);
console.log(`Intended: ${intended.symbol}`);
// 2. Check if we need to recover (did user already get intended token?)
if (received.address.toLowerCase() === intended.address.toLowerCase()) {
console.log('Already received intended token - no recovery needed');
return { success: true, recovered: false };
}
// 3. Get recovery quote
console.log('Getting recovery swap quote...');
const recoveryQuote = await getRecoveryQuote(status, originalQuote, userAddress);
// 4. Check if recovery is worthwhile (accounting for gas)
const gasCostEstimate = estimateGasCost(recoveryQuote);
const swapValue = estimateSwapValue(recoveryQuote);
if (gasCostEstimate > swapValue * 0.1) {
console.log('Gas cost too high relative to swap value');
console.log('Recommend keeping received token');
return {
success: true,
recovered: false,
reason: 'Gas cost exceeds 10% of swap value'
};
}
// 5. Execute recovery swap
console.log('Executing recovery swap...');
// Handle approval if needed
await ensureApproval(recoveryQuote, walletClient, publicClient);
// Execute swap
const { hash } = await executeTransaction(
walletClient,
publicClient,
recoveryQuote.transactionRequest
);
console.log(`Recovery swap sent: ${hash}`);
// 6. Poll recovery status (same-chain is usually fast)
const recoveryResult = await pollTransferStatus({
txHash: hash,
bridge: recoveryQuote.tool,
fromChain: received.chainId,
toChain: received.chainId
}, {
maxDuration: 5 * 60 * 1000 // 5 minutes for same-chain
});
return {
success: recoveryResult.success,
recovered: true,
recoveryTxHash: hash,
finalStatus: recoveryResult.status
};
}
Why Partial Completions Happen
| Cause | Explanation |
|---|
| Slippage exceeded | Price moved beyond tolerance during transfer time |
| Liquidity changed | DEX pool liquidity depleted |
| Token delist | Destination DEX no longer supports the pair |
| Contract issue | Destination DEX had temporary issue |
Prevention Strategies
1. Use Higher Slippage for Volatile Tokens
// For volatile tokens, increase slippage
const slippage = isVolatileToken(toToken) ? 0.02 : 0.005; // 2% vs 0.5%
2. Prefer Direct Bridge Routes
// Request routes without destination swap when possible
const quote = await getQuote({
...params,
toToken: 'USDC', // Bridge token directly instead of swapping
});
3. Check Route Feasibility Before Transfer
For large amounts, you can verify the destination swap is feasible by requesting a quote for just that leg:
// Get quote for just the destination swap
const destSwapQuote = await getQuote({
fromChain: toChain,
toChain: toChain,
fromToken: bridgeToken,
toToken: intendedToken,
fromAmount: amount
});
// If quote fails with NO_POSSIBLE_ROUTE, the destination swap may fail
// Consider using the bridge token as the final destination
User Communication Templates
Partial Completion Detected
Your transfer has partially completed.
✓ Bridged successfully
✗ Destination swap failed due to slippage
You received: 10.00 USDC on Arbitrum
You requested: ETH on Arbitrum
Would you like to swap your USDC to ETH now?
Estimated output: 0.0029 ETH
Recovery Not Available
Your transfer has partially completed.
You received: 10.00 TOKEN on Arbitrum
You requested: ETH on Arbitrum
Unfortunately, there's no direct swap route for TOKEN → ETH.
Your TOKEN balance is available in your wallet on Arbitrum.
Options:
1. Keep the TOKEN
2. Manually swap on a DEX that supports this token
Decision Tree
PARTIAL status received
│
▼
Is received token = intended token?
│
┌───┴───┐
│ │
Yes No
│ │
▼ ▼
Done Get recovery quote
│
▼
Quote available?
│
┌───┴───┐
│ │
Yes No
│ │
▼ ▼
Gas cost < 10% Inform user,
of value? suggest manual
│
┌───┴───┐
│ │
Yes No
│ │
▼ ▼
Execute Recommend keeping
recovery received token
Related Pages