From 0ee84db6cdab06054c6ae79c6d871bae0b4f3edb Mon Sep 17 00:00:00 2001 From: Akshay Rama Date: Mon, 25 May 2026 15:21:45 +0800 Subject: [PATCH] feat(trade): bypass gas check for high-value trades eligible for gasless routing Trades >= $10 USD can use solver-paid (gasless) routes such as Relay, so the native gas pre-check is no longer a blocker for them. The threshold is exported as GASLESS_MIN_TRADE_USD for easy future adjustment. When the check does fail (trade < $10, insufficient gas), the error now also suggests increasing the trade value as an alternative to topping up gas. Co-Authored-By: Claude Sonnet 4.6 --- .changeset/gasless-high-value-bypass.md | 5 +++ src/__tests__/trade-validation.test.js | 53 ++++++++++++++++++++++++- src/trade-validation.js | 15 ++++++- src/trading.js | 4 +- 4 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 .changeset/gasless-high-value-bypass.md diff --git a/.changeset/gasless-high-value-bypass.md b/.changeset/gasless-high-value-bypass.md new file mode 100644 index 00000000..d887aebc --- /dev/null +++ b/.changeset/gasless-high-value-bypass.md @@ -0,0 +1,5 @@ +--- +"nansen-cli": patch +--- + +Skip native gas pre-check for trades >= $10 USD, where gasless/solver-paid routes (e.g. Relay) are viable. When gas is insufficient on smaller trades, the error now also suggests increasing the trade value as an alternative to topping up gas. diff --git a/src/__tests__/trade-validation.test.js b/src/__tests__/trade-validation.test.js index 839dafce..4e7adc5f 100644 --- a/src/__tests__/trade-validation.test.js +++ b/src/__tests__/trade-validation.test.js @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import fs from 'fs'; import path from 'path'; import os from 'os'; -import { validateQuoteInput, fetchNativeBalance, fetchTokenBalance, validateBalance, resolvePercentAmount, validateGasBalance } from '../trade-validation.js'; +import { validateQuoteInput, fetchNativeBalance, fetchTokenBalance, validateBalance, resolvePercentAmount, validateGasBalance, GASLESS_MIN_TRADE_USD } from '../trade-validation.js'; describe('validateQuoteInput', () => { const validSolana = { @@ -873,6 +873,57 @@ describe('validateGasBalance', () => { const result = await validateGasBalance({ chain: 'solana', walletAddress: 'SomeWallet1111111111111111111111111111111111' }); expect(result.hasSufficientNative).toBe(true); }); + + it('bypasses gas check when trade value is >= GASLESS_MIN_TRADE_USD (gasless eligible)', async () => { + // fetch should NOT be called — the check is skipped before any RPC + global.fetch = vi.fn(); + + const result = await validateGasBalance({ + chain: 'solana', + walletAddress: 'SomeWallet1111111111111111111111111111111111', + tradeValueUsd: String(GASLESS_MIN_TRADE_USD), + }); + expect(result.hasSufficientNative).toBe(true); + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it('bypasses gas check when trade value is well above $10', async () => { + global.fetch = vi.fn(); + + const result = await validateGasBalance({ + chain: 'base', + walletAddress: '0x742d35Cc6bF4F3f4e0e3a8DD7e37ff4e4Be4E4B4', + tradeValueUsd: '500', + }); + expect(result.hasSufficientNative).toBe(true); + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it('still validates gas for trades below GASLESS_MIN_TRADE_USD', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ jsonrpc: '2.0', id: 1, result: { value: 0 } }), + }); + + await expect(validateGasBalance({ + chain: 'solana', + walletAddress: 'SomeWallet1111111111111111111111111111111111', + tradeValueUsd: String(GASLESS_MIN_TRADE_USD - 0.01), + })).rejects.toThrow(/Insufficient SOL for gas/); + }); + + it('error message includes gasless suggestion when gas is low', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ jsonrpc: '2.0', id: 1, result: { value: 0 } }), + }); + + await expect(validateGasBalance({ + chain: 'solana', + walletAddress: 'SomeWallet1111111111111111111111111111111111', + tradeValueUsd: '5.00', + })).rejects.toThrow(new RegExp(`\\$${GASLESS_MIN_TRADE_USD}\\+`)); + }); }); describe('quote handler balance validation integration', () => { diff --git a/src/trade-validation.js b/src/trade-validation.js index 04810253..b8665a79 100644 --- a/src/trade-validation.js +++ b/src/trade-validation.js @@ -136,6 +136,10 @@ const FEE_BUFFER = { solana: 0.005, base: 0.00004 }; const HIGH_PERCENTAGE_THRESHOLD = 95; const AUTO_ADJUST_THRESHOLD_PERCENT = 2; +// Trades at or above this USD value can use gasless/solver-paid routes (e.g. Relay), +// so the native gas pre-check is skipped for them. +export const GASLESS_MIN_TRADE_USD = 10; + /** * Check if an address is USDC or the native token for a chain (case-insensitive for EVM). */ @@ -306,12 +310,19 @@ export async function resolvePercentAmount({ chain, from, walletAddress, percent * * Returns { hasSufficientNative } or throws on validation failure. * Best-effort: if RPC fails, returns passing result. + * + * Gasless bypass: trades >= $10 USD can use solver-paid options (e.g. Relay), + * so the gas check is skipped in that case. */ -export async function validateGasBalance({ chain, walletAddress }) { +export async function validateGasBalance({ chain, walletAddress, tradeValueUsd }) { const normalizedChain = chain.toLowerCase(); const minGas = MIN_GAS_AMOUNTS[normalizedChain]; if (minGas === undefined) return { hasSufficientNative: true }; + // High-value trades can use gasless/solver-paid routes — skip the check. + const tradeUsd = parseFloat(tradeValueUsd) || 0; + if (tradeUsd >= GASLESS_MIN_TRADE_USD) return { hasSufficientNative: true }; + const balance = await fetchNativeBalance(normalizedChain, walletAddress); // RPC failure — proceed without validation. @@ -323,7 +334,7 @@ export async function validateGasBalance({ chain, walletAddress }) { const symbol = NATIVE_SYMBOLS[normalizedChain] || 'native token'; throw new Error( - `Insufficient ${symbol} for gas fees. Wallet has ${balance} ${symbol} but needs at least ${minGas} ${symbol}. Fund the wallet before trading.` + `Insufficient ${symbol} for gas fees. Wallet has ${balance} ${symbol} but needs at least ${minGas} ${symbol}. Either fund the wallet with ${symbol} or trade a value of $${GASLESS_MIN_TRADE_USD}+ to use gasless options.` ); } diff --git a/src/trading.js b/src/trading.js index 2c805ba2..7c09acef 100644 --- a/src/trading.js +++ b/src/trading.js @@ -1331,8 +1331,10 @@ CROSS-CHAIN NOTES (when using --to-chain): response.quotes.forEach((q, i) => log(formatQuote(q, i))); // Gas balance validation — check that the wallet has enough native token for gas. + // High-value trades (>= $10) can use gasless/solver-paid routes and bypass this check. try { - await validateGasBalance({ chain, walletAddress }); + const tradeValueUsd = response.quotes[0]?.inUsdValue; + await validateGasBalance({ chain, walletAddress, tradeValueUsd }); } catch (gasErr) { throw new CommandError(`Error: ${gasErr.message}`, 'INSUFFICIENT_GAS'); }