mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
Both came out as an empty list, so WETH/USDC on Ethereum — one of the deepest pools in existence — was reported as "no venue here holds this pair" whenever its RPC was down. That sends a reader looking for liquidity instead of at their endpoint, which is the same mistake as blaming the pair for an exact-output request and blaming an upstream registry for our own chain. The venues already distinguished the two answers: no liquidity is (nil, nil), a chain they could not reach is (nil, err). QueryAllVenues discarded both. It now returns the reason, and only when EVERY arm it asked failed — one arm down while another answers is a quote, not an outage. It also stops returning the routing label, which named V4_NATIVE for a V3 pool on somebody else's chain and was read by one test assertion and nothing else. Underneath, EVMClient marks the answers it never received: a connection failure, a 5xx, a 429, a body that is not JSON-RPC. A contract answering by REVERTING stays what it always was — this pool does not exist — because three of the four V3 fee tiers revert on most pairs and that is the ordinary case. Measured with chain 1 pointed at a closed port: chain 1 answers 502 naming the dial error, 96369 still quotes 997001483075056712, a pair with no pool still answers 404, and /providers stays 200 so one dark chain does not take a pod out of its own Service. Co-authored-by: Hanzo Dev <[email protected]>
143 lines
4.5 KiB
Go
143 lines
4.5 KiB
Go
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
)
|
|
|
|
// QuoterV2.quoteExactInputSingle((address,address,uint256,uint24,uint160))
|
|
//
|
|
// The struct is a single static tuple, so it encodes inline: five words after
|
|
// the selector, no offset. The older QuoterV1 took the same five values as
|
|
// flat arguments under a different selector; a chain still running V1 answers
|
|
// nothing to this one, which reads as a pair with no pool rather than a wrong
|
|
// answer.
|
|
var selectorV3QuoteExactInputSingle = lxrMustDecodeHex("c6a5026a")
|
|
|
|
// The fee tiers a V3 deployment publishes, in hundredths of a basis point.
|
|
// A quote asks each and keeps the best, because which tier holds the liquidity
|
|
// for a pair is a fact about that pair and not about the chain.
|
|
var v3FeeTiers = []uint32{100, 500, 3000, 10000}
|
|
|
|
// UniswapV3Venue quotes from any Uniswap V3-compatible QuoterV2.
|
|
//
|
|
// It exists because the gateway could read a V2 router and the native
|
|
// precompiles and nothing in between, so every V3 market — which on most
|
|
// chains is where the liquidity is — was invisible. Reading the quoter over
|
|
// JSON-RPC needs no API key and no upstream account: the pool is the source.
|
|
type UniswapV3Venue struct {
|
|
evm *EVMClient
|
|
quoter string
|
|
router string
|
|
venueName string
|
|
tiers []uint32
|
|
}
|
|
|
|
// UniswapV3Config configures a V3 venue.
|
|
type UniswapV3Config struct {
|
|
// RPCURL is the EVM RPC endpoint.
|
|
RPCURL string
|
|
// QuoterAddress is the QuoterV2 contract (0x-prefixed).
|
|
QuoterAddress string
|
|
// RouterAddress is the SwapRouter02 that executes what the quoter priced.
|
|
// Empty means this venue quotes and does not build.
|
|
RouterAddress string
|
|
// Name identifies this venue (e.g. "uniswap_v3", "pancake_v3").
|
|
Name string
|
|
// FeeTiers overrides the tiers asked. Empty means the four V3 publishes.
|
|
FeeTiers []uint32
|
|
}
|
|
|
|
// NewUniswapV3Venue creates a venue that quotes via a QuoterV2.
|
|
func NewUniswapV3Venue(cfg UniswapV3Config) *UniswapV3Venue {
|
|
name := cfg.Name
|
|
if name == "" {
|
|
name = "uniswap_v3"
|
|
}
|
|
tiers := cfg.FeeTiers
|
|
if len(tiers) == 0 {
|
|
tiers = v3FeeTiers
|
|
}
|
|
return &UniswapV3Venue{
|
|
evm: NewEVMClient(cfg.RPCURL),
|
|
quoter: cfg.QuoterAddress,
|
|
router: cfg.RouterAddress,
|
|
venueName: name,
|
|
tiers: tiers,
|
|
}
|
|
}
|
|
|
|
func (v *UniswapV3Venue) Name() string { return v.venueName }
|
|
func (v *UniswapV3Venue) IsExecutable() bool { return true }
|
|
|
|
func (v *UniswapV3Venue) Quote(ctx context.Context, req VenueQuoteRequest) (*VenueQuote, error) {
|
|
if req.Type != VenueQuoteTypeExactInput {
|
|
return nil, nil
|
|
}
|
|
|
|
amountIn, ok := new(big.Int).SetString(req.Amount, 10)
|
|
if !ok || amountIn.Sign() <= 0 {
|
|
return nil, fmt.Errorf("invalid amount: %s", req.Amount)
|
|
}
|
|
|
|
inAddr, err := lxrDecodeAddress(req.TokenIn)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tokenIn: %w", err)
|
|
}
|
|
outAddr, err := lxrDecodeAddress(req.TokenOut)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tokenOut: %w", err)
|
|
}
|
|
|
|
best := new(big.Int)
|
|
var bestFee uint32
|
|
var unreachable error
|
|
for _, fee := range v.tiers {
|
|
// (tokenIn, tokenOut, amountIn, fee, sqrtPriceLimitX96) — a static
|
|
// tuple, so five words inline. The price limit is zero: no bound.
|
|
calldata := make([]byte, 0, 4+32*5)
|
|
calldata = append(calldata, selectorV3QuoteExactInputSingle...)
|
|
calldata = append(calldata, lxrPadAddress(inAddr)...)
|
|
calldata = append(calldata, lxrPadAddress(outAddr)...)
|
|
calldata = append(calldata, lxrPadUint256(amountIn)...)
|
|
calldata = append(calldata, lxrPadUint256(new(big.Int).SetUint64(uint64(fee)))...)
|
|
calldata = append(calldata, lxrPadUint256(big.NewInt(0))...)
|
|
|
|
result, err := v.evm.CallContract(ctx, v.quoter, calldata)
|
|
if errors.Is(err, ErrUnreachable) {
|
|
// Not an answer at all. Every tier will say the same, so the loop
|
|
// still finishes and the reason is kept for the end.
|
|
unreachable = err
|
|
continue
|
|
}
|
|
if err != nil || len(result) < 32 {
|
|
// A tier with no pool reverts. That is the ordinary answer to
|
|
// three of the four tiers on most pairs, not a failure to report.
|
|
continue
|
|
}
|
|
|
|
out := new(big.Int).SetBytes(result[0:32])
|
|
if out.Cmp(best) > 0 {
|
|
best = out
|
|
bestFee = fee
|
|
}
|
|
}
|
|
|
|
if best.Sign() <= 0 {
|
|
// No tier held anything. If nothing was ever reached, that is why.
|
|
return nil, unreachable
|
|
}
|
|
|
|
return &VenueQuote{
|
|
Venue: v.venueName,
|
|
AmountOut: best.String(),
|
|
// The tier the quote came from, in hundredths of a basis point, which
|
|
// is the unit V3 states a fee in.
|
|
Fee: fmt.Sprintf("%d", bestFee),
|
|
GasEstimate: "180000",
|
|
Executable: true,
|
|
}, nil
|
|
}
|