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:
2026-01-06 01:44:11 +07:00
parent 09a6a13eb1
commit 55eb1e7086
3 changed files with 177 additions and 7 deletions

View 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

View File

@@ -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) ||

View File

@@ -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)
}
})
}) })