Enhance token handling and logging in GMX plugin
- Updated token retrieval logic to ensure non-synthetic tokens are prioritized for swaps, improving accuracy in token selection. - Added detailed logging for token data, including assetSymbol and baseSymbol, to enhance visibility during token lookups. - Introduced a new test case to validate the successful swap of USDC to BTC, confirming the resolution to the non-synthetic WBTC token. - Improved error handling for token lookups, providing clearer feedback when a valid token symbol is not found.
This commit is contained in:
114
src/Managing.Web3Proxy/SYNTHETIC-TOKEN-FIX.md
Normal file
114
src/Managing.Web3Proxy/SYNTHETIC-TOKEN-FIX.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# Synthetic Token Fix for GMX Swaps
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
When attempting to swap tokens to/from BTC on GMX, users were encountering the following error:
|
||||||
|
```
|
||||||
|
Cannot swap to synthetic token BTC. Synthetic tokens are index tokens and can only be obtained by opening a long position. Please use the open-position endpoint instead.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
In the GMX token configuration (`src/generated/gmxsdk/configs/tokens.ts`), there are **two BTC tokens**:
|
||||||
|
|
||||||
|
1. **Synthetic BTC** (Index Token)
|
||||||
|
- Symbol: `BTC`
|
||||||
|
- Address: `0x47904963fc8b2340414262125aF798B9655E58Cd`
|
||||||
|
- `isSynthetic: true`
|
||||||
|
- Cannot be swapped directly
|
||||||
|
- Can only be obtained by opening a long position
|
||||||
|
|
||||||
|
2. **Wrapped Bitcoin (WBTC)**
|
||||||
|
- Symbol: `BTC`
|
||||||
|
- Asset Symbol: `WBTC`
|
||||||
|
- Base Symbol: `BTC`
|
||||||
|
- Address: `0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f`
|
||||||
|
- `isSynthetic: false` (or not set)
|
||||||
|
- **CAN be swapped**
|
||||||
|
|
||||||
|
Both tokens have the same symbol "BTC", which was causing the wrong token to be selected for swaps.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
Updated the `getTokenDataFromTicker()` function to **explicitly filter out synthetic tokens** when fetching tokens for swaps by using the `isSynthetic: false` parameter in `getTokenBySymbol()`.
|
||||||
|
|
||||||
|
### Code Changes
|
||||||
|
|
||||||
|
#### File: `src/plugins/custom/gmx.ts`
|
||||||
|
|
||||||
|
**Function: `getTokenDataFromTicker()`**
|
||||||
|
|
||||||
|
Added the `isSynthetic: false` filter to all `getTokenBySymbol()` calls:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// First try v2 tokens with explicit isSynthetic: false filter
|
||||||
|
token = getTokenBySymbol(arbitrum.id, ticker, { version: "v2", isSynthetic: false });
|
||||||
|
|
||||||
|
// Fallback: try without version constraint but still filter synthetic tokens
|
||||||
|
token = getTokenBySymbol(arbitrum.id, ticker, { isSynthetic: false });
|
||||||
|
|
||||||
|
// Last resort: try to find by baseSymbol
|
||||||
|
token = getTokenBySymbol(arbitrum.id, ticker, {
|
||||||
|
isSynthetic: false,
|
||||||
|
symbolType: "baseSymbol"
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
1. When a user requests to swap BTC (or any token), the system now:
|
||||||
|
- Calls `getTokenBySymbol()` with `isSynthetic: false`
|
||||||
|
- This filter ensures only non-synthetic tokens are returned
|
||||||
|
- For BTC, it will return the Wrapped Bitcoin (WBTC) token instead of the synthetic BTC token
|
||||||
|
|
||||||
|
2. The filter is applied at the token lookup level, ensuring:
|
||||||
|
- **Swaps**: Always use non-synthetic tokens (can be swapped)
|
||||||
|
- **Positions**: Can still use synthetic index tokens (via `getMarketInfoFromTicker()`)
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Test Case
|
||||||
|
Added a test in `test/plugins/swap-tokens.test.ts` to verify:
|
||||||
|
- USDC can be swapped to BTC
|
||||||
|
- The system automatically selects the non-synthetic WBTC token
|
||||||
|
- The swap completes without throwing the synthetic token error
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
it('should swap USDC to BTC successfully (BTC resolves to non-synthetic WBTC token)', async () => {
|
||||||
|
const result = await swapGmxTokensImpl(
|
||||||
|
sdk,
|
||||||
|
'USDC',
|
||||||
|
'BTC', // isSynthetic: false filter selects the non-synthetic WBTC token
|
||||||
|
5,
|
||||||
|
'market',
|
||||||
|
undefined,
|
||||||
|
0.5
|
||||||
|
);
|
||||||
|
assert.strictEqual(result, 'swap_order_created');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
### Before Fix
|
||||||
|
- Swaps to/from BTC would fail with synthetic token error
|
||||||
|
- Users had to manually specify "WBTC" instead of "BTC"
|
||||||
|
|
||||||
|
### After Fix
|
||||||
|
- Users can use "BTC" in swap requests
|
||||||
|
- System automatically uses the correct non-synthetic WBTC token
|
||||||
|
- No more synthetic token errors for swaps
|
||||||
|
- Opening positions still works correctly with synthetic index tokens
|
||||||
|
|
||||||
|
## Key Takeaways
|
||||||
|
|
||||||
|
1. **Always filter synthetic tokens for swaps**: Use `isSynthetic: false` when calling `getTokenBySymbol()` for swap operations
|
||||||
|
2. **Token symbol ambiguity**: Multiple tokens can have the same symbol but different properties (synthetic vs non-synthetic)
|
||||||
|
3. **Proper token selection**: The `isSynthetic` filter is crucial for selecting the correct token for different operations
|
||||||
|
4. **Following GMX patterns**: The GMX SDK itself uses `isSynthetic: false` in markets.ts (lines 1205-1210) when fetching long/short tokens
|
||||||
|
|
||||||
|
## Related Files
|
||||||
|
- `src/plugins/custom/gmx.ts` - Main swap implementation
|
||||||
|
- `src/generated/gmxsdk/configs/tokens.ts` - Token configuration
|
||||||
|
- `test/plugins/swap-tokens.test.ts` - Test cases
|
||||||
|
|
||||||
|
## Date
|
||||||
|
January 6, 2026
|
||||||
|
|
||||||
@@ -1233,6 +1233,10 @@ function getMarketInfoFromTicker(ticker: string, marketsInfoData: MarketsInfoDat
|
|||||||
ticker = "WETH";
|
ticker = "WETH";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: For BTC, we don't need manual conversion anymore since getTokenBySymbol
|
||||||
|
// will automatically select the non-synthetic token when we pass isSynthetic: false
|
||||||
|
// However, for market info lookups, we may need the synthetic index token
|
||||||
|
// So we don't filter synthetic tokens here (markets can have synthetic index tokens)
|
||||||
const token = getTokenBySymbol(arbitrum.id, ticker);
|
const token = getTokenBySymbol(arbitrum.id, ticker);
|
||||||
const marketInfo = getMarketByIndexToken(token.address);
|
const marketInfo = getMarketByIndexToken(token.address);
|
||||||
|
|
||||||
@@ -1251,22 +1255,43 @@ function getMarketInfoFromTicker(ticker: string, marketsInfoData: MarketsInfoDat
|
|||||||
|
|
||||||
export function getTokenDataFromTicker(ticker: string, tokensData: TokensData): TokenData {
|
export function getTokenDataFromTicker(ticker: string, tokensData: TokensData): TokenData {
|
||||||
console.log(`🔍 Looking up token for ticker: ${ticker}`);
|
console.log(`🔍 Looking up token for ticker: ${ticker}`);
|
||||||
// Try to find the token without synthetic filter to support both synthetic and non-synthetic tokens
|
|
||||||
// First try v2 tokens (preferred)
|
// IMPORTANT: For swaps, we must ONLY use non-synthetic tokens (isSynthetic: false)
|
||||||
|
// Synthetic tokens (like BTC at 0x47904963fc8b2340414262125aF798B9655E58Cd) cannot be swapped
|
||||||
|
// We need to use wrapped tokens (like WBTC at 0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f) instead
|
||||||
|
// The isSynthetic filter ensures we get the correct token
|
||||||
let token;
|
let token;
|
||||||
try {
|
try {
|
||||||
token = getTokenBySymbol(arbitrum.id, ticker, { version: "v2" });
|
// First try v2 tokens with explicit isSynthetic: false filter
|
||||||
|
token = getTokenBySymbol(arbitrum.id, ticker, { version: "v2", isSynthetic: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If not found in v2, try without version constraint
|
// If not found in v2, try without version constraint but still filter synthetic tokens
|
||||||
token = getTokenBySymbol(arbitrum.id, ticker);
|
try {
|
||||||
|
token = getTokenBySymbol(arbitrum.id, ticker, { isSynthetic: false });
|
||||||
|
} catch (innerError) {
|
||||||
|
// Last resort: try to find by assetSymbol (e.g., WBTC for BTC)
|
||||||
|
// This handles cases where the user passes "BTC" but we need to find the token with assetSymbol "WBTC"
|
||||||
|
try {
|
||||||
|
token = getTokenBySymbol(arbitrum.id, ticker, {
|
||||||
|
isSynthetic: false,
|
||||||
|
symbolType: "baseSymbol"
|
||||||
|
});
|
||||||
|
} catch (finalError) {
|
||||||
|
throw new Error(`Token not found for ticker: ${ticker}. Please ensure you're using a valid non-synthetic token symbol.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`📋 Token found:`, {
|
console.log(`📋 Token found:`, {
|
||||||
symbol: token.symbol,
|
symbol: token.symbol,
|
||||||
|
assetSymbol: token.assetSymbol,
|
||||||
|
baseSymbol: token.baseSymbol,
|
||||||
address: token.address,
|
address: token.address,
|
||||||
decimals: token.decimals,
|
decimals: token.decimals,
|
||||||
isNative: token.isNative,
|
isNative: token.isNative,
|
||||||
isSynthetic: token.isSynthetic
|
isSynthetic: token.isSynthetic
|
||||||
});
|
});
|
||||||
|
|
||||||
const tokenData = getByKey(tokensData, token.address);
|
const tokenData = getByKey(tokensData, token.address);
|
||||||
console.log(`📊 Token data:`, {
|
console.log(`📊 Token data:`, {
|
||||||
address: tokenData?.address,
|
address: tokenData?.address,
|
||||||
@@ -1274,6 +1299,7 @@ export function getTokenDataFromTicker(ticker: string, tokensData: TokensData):
|
|||||||
isNative: tokenData?.isNative,
|
isNative: tokenData?.isNative,
|
||||||
symbol: tokenData?.symbol
|
symbol: tokenData?.symbol
|
||||||
});
|
});
|
||||||
|
|
||||||
return tokenData;
|
return tokenData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1939,8 +1965,6 @@ export const getSpotPositionHistoryImpl = async (
|
|||||||
positions.push(position);
|
positions.push(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`📊 Positions:`, positions);
|
|
||||||
|
|
||||||
if (ticker) {
|
if (ticker) {
|
||||||
positions = positions.filter(p =>
|
positions = positions.filter(p =>
|
||||||
p.ticker === (ticker as any) ||
|
p.ticker === (ticker as any) ||
|
||||||
|
|||||||
@@ -32,4 +32,36 @@ describe('swap tokens implementation', () => {
|
|||||||
assert.fail(error.message)
|
assert.fail(error.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should swap USDC to BTC successfully (BTC resolves to non-synthetic WBTC token)', async () => {
|
||||||
|
try {
|
||||||
|
const testAccount = '0x932167388dD9aad41149b3cA23eBD489E2E2DD78'
|
||||||
|
|
||||||
|
const sdk = await getClientForAddress(testAccount)
|
||||||
|
|
||||||
|
console.log('\n🔄 Starting USDC → BTC swap...\n')
|
||||||
|
console.log('Note: BTC ticker resolves to the non-synthetic WBTC token (address: 0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f)')
|
||||||
|
console.log('The synthetic BTC token (address: 0x47904963fc8b2340414262125aF798B9655E58Cd) is filtered out\n')
|
||||||
|
|
||||||
|
// Swap $5 worth of USDC to BTC
|
||||||
|
// The isSynthetic: false filter ensures we get the wrapped BTC (WBTC) token
|
||||||
|
// At ~$100k BTC price, this should give us ~0.00005 WBTC
|
||||||
|
const result = await swapGmxTokensImpl(
|
||||||
|
sdk,
|
||||||
|
'USDC',
|
||||||
|
'BTC', // isSynthetic: false filter selects the non-synthetic WBTC token
|
||||||
|
5, // $5 worth of USDC
|
||||||
|
'market',
|
||||||
|
undefined,
|
||||||
|
0.5
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('\n✅ USDC → BTC swap successful! Used non-synthetic WBTC token\n')
|
||||||
|
assert.strictEqual(typeof result, 'string')
|
||||||
|
assert.strictEqual(result, 'swap_order_created')
|
||||||
|
} catch (error) {
|
||||||
|
console.log('\n❌ USDC → BTC swap failed:', error)
|
||||||
|
assert.fail(error.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user