mirror of
https://github.com/luxfi/maker.git
synced 2026-10-30 16:20:04 +00:00
Modules declare go 1.27.1 and the build images follow. go fix applied the modernizers; omitzero is not applied, since it changes what the JSON says. Co-authored-by: Hanzo Dev <[email protected]>
585 lines
23 KiB
Go
585 lines
23 KiB
Go
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package maker
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
_ "embed"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/luxfi/crypto"
|
|
dex "github.com/luxfi/precompile/dex"
|
|
"github.com/luxfi/sdk/keys"
|
|
|
|
ethereum "github.com/luxfi/geth"
|
|
"github.com/luxfi/geth/accounts/abi"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/geth/core/types"
|
|
"github.com/luxfi/geth/ethclient"
|
|
)
|
|
|
|
// pool9999.go is the C-Chain 0x9999 money-path client — the EVM transport the
|
|
// coherence service pegs through. It is the orthogonal twin of dex.go (the native
|
|
// D-Chain CLOB transport): the D-Chain client speaks frozen clob_* frames over
|
|
// luxd's HTTP router; THIS client speaks the V4 PoolManager precompile (0x9999)
|
|
// over plain JSON-RPC, on any Lux EVM chain. One client per venue; the coherence
|
|
// service uses this one because the task pegs markets on the C-Chain via 0x9999.
|
|
//
|
|
// The protocol surface (precompile address, selectors, pool-key encoding) is
|
|
// imported from github.com/luxfi/precompile/dex — this file owns only the client
|
|
// concern: build calldata, sign, broadcast, read. It is the proven trader-pkg
|
|
// money path (initialize / swapDeposit / swapPlace / swap / DEXFill) re-homed into
|
|
// the private maker, EXTENDED with the two-sided-maker primitives the continuous
|
|
// loop needs: swapCancel, getOpenOrders, getBestBidAsk.
|
|
|
|
// PoolManager is the V4 PoolManager precompile — the DEX money path, identical on
|
|
// every Lux EVM chain (0x…9999).
|
|
var PoolManager = common.HexToAddress(dex.DEXPoolManagerAddress)
|
|
|
|
// gasFloorBump is added to the suggested gas price so a legacy tx always clears
|
|
// the EIP-1559 base-fee floor on a fresh or idle chain.
|
|
var gasFloorBump = big.NewInt(25_000_000_000)
|
|
|
|
// dexFillSig is the canonical settlement signal: DEXFill(bytes32 indexed poolId,
|
|
// address indexed taker, uint256 amountOut, uint256 blockNumber).
|
|
var dexFillSig = common.BytesToHash(crypto.Keccak256([]byte("DEXFill(bytes32,address,uint256,uint256)")))
|
|
|
|
// Account is a secp256k1 signing identity on a Lux EVM chain.
|
|
type Account struct {
|
|
key *ecdsa.PrivateKey
|
|
Addr common.Address
|
|
}
|
|
|
|
// AccountFromHex builds an account from a hex secp256k1 private key.
|
|
func AccountFromHex(hexKey string) (Account, error) {
|
|
k, err := crypto.HexToECDSA(strings.TrimPrefix(hexKey, "0x"))
|
|
if err != nil {
|
|
return Account{}, fmt.Errorf("parse key: %w", err)
|
|
}
|
|
return Account{key: k, Addr: common.BytesToAddress(crypto.PubkeyToAddress(k.PublicKey).Bytes())}, nil
|
|
}
|
|
|
|
// AccountFromMnemonic derives the account at a BIP-44 index using the SAME
|
|
// derivation genesis uses (github.com/luxfi/sdk/keys, m/44'/9000'/0'/0/index), so
|
|
// a derived account is exactly the genesis-funded one at that index.
|
|
func AccountFromMnemonic(mnemonic string, index uint32) (Account, error) {
|
|
raw, err := keys.DeriveKeyFromMnemonic(strings.TrimSpace(mnemonic), index)
|
|
if err != nil {
|
|
return Account{}, fmt.Errorf("derive index %d: %w", index, err)
|
|
}
|
|
k, err := crypto.ToECDSA(raw)
|
|
if err != nil {
|
|
return Account{}, fmt.Errorf("to ecdsa index %d: %w", index, err)
|
|
}
|
|
return Account{key: k, Addr: common.BytesToAddress(crypto.PubkeyToAddress(k.PublicKey).Bytes())}, nil
|
|
}
|
|
|
|
// Pool9999 is a net-agnostic 0x9999 client over one or more C-Chain JSON-RPC
|
|
// endpoints. When several endpoints are given (comma-separated), every tx is
|
|
// broadcast to ALL of them, so on a net whose validators don't gossip the mempool
|
|
// the block proposer always receives the tx. Reads use the first endpoint.
|
|
type Pool9999 struct {
|
|
ctx context.Context
|
|
eth *ethclient.Client // primary: reads + receipt polling
|
|
bcast []*ethclient.Client // all endpoints (incl primary) for broadcast
|
|
chainID *big.Int
|
|
erc abi.ABI
|
|
waitFor time.Duration // receipt poll budget (cold-builder tolerant)
|
|
waitEvery time.Duration // receipt poll interval
|
|
decCache sync.Map // common.Address -> int (ERC-20 decimals; immutable, so memoized)
|
|
}
|
|
|
|
// sigERC20Decimals is the ERC-20 decimals() selector, derived from the signature so
|
|
// it can never drift from a hand-copied hex literal.
|
|
var sigERC20Decimals = crypto.Keccak256([]byte("decimals()"))[:4]
|
|
|
|
// DialPool9999 connects to one or more Lux EVM C-Chain RPCs (comma-separated) and
|
|
// reads the chain id from the first. Every endpoint is a broadcast target so a tx
|
|
// reaches every validator regardless of mempool gossip. waitFor bounds the receipt
|
|
// wait (a fresh/idle chain's first block can be tens of seconds out).
|
|
func DialPool9999(ctx context.Context, rpcURL string, waitFor time.Duration) (*Pool9999, error) {
|
|
if waitFor <= 0 {
|
|
waitFor = 120 * time.Second
|
|
}
|
|
var bcast []*ethclient.Client
|
|
for u := range strings.SplitSeq(rpcURL, ",") {
|
|
u = strings.TrimSpace(u)
|
|
if u == "" {
|
|
continue
|
|
}
|
|
cl, err := ethclient.DialContext(ctx, u)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("dial %s: %w", u, err)
|
|
}
|
|
bcast = append(bcast, cl)
|
|
}
|
|
if len(bcast) == 0 {
|
|
return nil, fmt.Errorf("no rpc endpoints")
|
|
}
|
|
chainID, err := bcast[0].ChainID(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("chainID: %w", err)
|
|
}
|
|
ercABI, err := abi.JSON(strings.NewReader(erc20ABI))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("erc20 abi: %w", err)
|
|
}
|
|
return &Pool9999{
|
|
ctx: ctx, eth: bcast[0], bcast: bcast, chainID: chainID, erc: ercABI,
|
|
waitFor: waitFor, waitEvery: 500 * time.Millisecond,
|
|
}, nil
|
|
}
|
|
|
|
// ChainID is the EVM chain id read at dial time.
|
|
func (c *Pool9999) ChainID() *big.Int { return c.chainID }
|
|
|
|
// Block returns the latest accepted block height.
|
|
func (c *Pool9999) Block() (uint64, error) { return c.eth.BlockNumber(c.ctx) }
|
|
|
|
// NativeBalance returns an account's native LUX balance (wei).
|
|
func (c *Pool9999) NativeBalance(a common.Address) (*big.Int, error) {
|
|
return c.eth.BalanceAt(c.ctx, a, nil)
|
|
}
|
|
|
|
// GasPrice is the suggested gas price plus the floor bump — the legacy-tx price
|
|
// every write uses so it always clears the EIP-1559 base-fee floor on a fresh or
|
|
// idle chain. Exposed so a pipelined sender fetches it once per burst.
|
|
func (c *Pool9999) GasPrice() (*big.Int, error) {
|
|
gp, err := c.eth.SuggestGasPrice(c.ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("gas price: %w", err)
|
|
}
|
|
return new(big.Int).Add(gp, gasFloorBump), nil
|
|
}
|
|
|
|
// PendingNonce is the next usable nonce for an account (pending pool included) —
|
|
// a pipelined sender reads it once, then increments locally to keep many txs in
|
|
// flight without a nonce round-trip per submission.
|
|
func (c *Pool9999) PendingNonce(a common.Address) (uint64, error) {
|
|
return c.eth.PendingNonceAt(c.ctx, a)
|
|
}
|
|
|
|
// signTx builds and signs a legacy tx at an explicit nonce and gas price.
|
|
func (c *Pool9999) signTx(from Account, to *common.Address, value *big.Int, data []byte, gas, nonce uint64, gp *big.Int) (*types.Transaction, error) {
|
|
var tx *types.Transaction
|
|
if to == nil {
|
|
tx = types.NewContractCreation(nonce, value, gas, gp, data)
|
|
} else {
|
|
tx = types.NewTransaction(nonce, *to, value, gas, gp, data)
|
|
}
|
|
return types.SignTx(tx, types.LatestSignerForChainID(c.chainID), from.key)
|
|
}
|
|
|
|
// broadcast sends a signed tx to every endpoint; success if any endpoint accepts.
|
|
func (c *Pool9999) broadcast(signed *types.Transaction) error {
|
|
var sent bool
|
|
var lastErr error
|
|
for _, cl := range c.bcast {
|
|
if err := cl.SendTransaction(c.ctx, signed); err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
sent = true
|
|
}
|
|
if !sent {
|
|
return fmt.Errorf("broadcast: %w", lastErr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// send signs, broadcasts to every endpoint, and waits for the receipt of a single
|
|
// tx. A nil `to` is a contract creation.
|
|
func (c *Pool9999) send(from Account, to *common.Address, value *big.Int, data []byte, gas uint64) (*types.Receipt, error) {
|
|
gp, err := c.GasPrice()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nonce, err := c.eth.PendingNonceAt(c.ctx, from.Addr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("nonce: %w", err)
|
|
}
|
|
signed, err := c.signTx(from, to, value, data, gas, nonce, gp)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sign: %w", err)
|
|
}
|
|
if err := c.broadcast(signed); err != nil {
|
|
return nil, err
|
|
}
|
|
return c.wait(signed.Hash())
|
|
}
|
|
|
|
// SwapAsync signs and broadcasts an exact-input CLOB swap at an EXPLICIT nonce and
|
|
// gas price and returns immediately with the tx hash (no receipt wait). It is the
|
|
// pipelined twin of Swap: a load driver reads GasPrice + PendingNonce once, then
|
|
// fires a stream of SwapAsync calls (nonce++, same gp) to keep many settlements in
|
|
// flight from one sender — the only way to measure settlement throughput above the
|
|
// one-tx-per-round-trip floor Swap imposes.
|
|
func (c *Pool9999) SwapAsync(from Account, key dex.PoolKey, zeroForOne bool, amountIn int64, minOut, nonce uint64, gp *big.Int) (common.Hash, error) {
|
|
signed, err := c.signTx(from, &PoolManager, big.NewInt(0),
|
|
swapData(key, zeroForOne, amountIn, dex.EncodeMinOutHookData(minOut)), 3_000_000, nonce, gp)
|
|
if err != nil {
|
|
return common.Hash{}, err
|
|
}
|
|
if err := c.broadcast(signed); err != nil {
|
|
return common.Hash{}, err
|
|
}
|
|
return signed.Hash(), nil
|
|
}
|
|
|
|
// Nonce is the account's latest MINED nonce (excludes pending) — paired with
|
|
// PendingNonce it bounds the set of stuck pending txs a drain must replace.
|
|
func (c *Pool9999) Nonce(a common.Address) (uint64, error) {
|
|
return c.eth.NonceAt(c.ctx, a, nil)
|
|
}
|
|
|
|
// SendNativeAsync signs and broadcasts a value transfer at an EXPLICIT nonce and
|
|
// gas price without waiting — used to REPLACE a stuck pending tx at that nonce
|
|
// with a cheap 21k no-op (higher gp evicts it), draining a mempool backlog fast.
|
|
func (c *Pool9999) SendNativeAsync(from Account, to common.Address, amount *big.Int, nonce uint64, gp *big.Int) (common.Hash, error) {
|
|
signed, err := c.signTx(from, &to, amount, nil, 21000, nonce, gp)
|
|
if err != nil {
|
|
return common.Hash{}, err
|
|
}
|
|
if err := c.broadcast(signed); err != nil {
|
|
return common.Hash{}, err
|
|
}
|
|
return signed.Hash(), nil
|
|
}
|
|
|
|
// TryReceipt returns (receipt, true) if the tx is mined, (nil, false) if it is
|
|
// still pending or unknown — a non-blocking poll for a pipelined driver.
|
|
func (c *Pool9999) TryReceipt(h common.Hash) (*types.Receipt, bool) {
|
|
r, err := c.eth.TransactionReceipt(c.ctx, h)
|
|
if err != nil || r == nil {
|
|
return nil, false
|
|
}
|
|
return r, true
|
|
}
|
|
|
|
// wait polls for a receipt up to waitFor.
|
|
func (c *Pool9999) wait(hash common.Hash) (*types.Receipt, error) {
|
|
deadline := time.Now().Add(c.waitFor)
|
|
for time.Now().Before(deadline) {
|
|
select {
|
|
case <-c.ctx.Done():
|
|
return nil, c.ctx.Err()
|
|
default:
|
|
}
|
|
r, err := c.eth.TransactionReceipt(c.ctx, hash)
|
|
if err == nil && r != nil {
|
|
return r, nil
|
|
}
|
|
time.Sleep(c.waitEvery)
|
|
}
|
|
return nil, fmt.Errorf("receipt %s not mined within %s", hash.Hex(), c.waitFor)
|
|
}
|
|
|
|
// mustOK wraps send and turns a non-success receipt into an error.
|
|
func (c *Pool9999) mustOK(from Account, to *common.Address, value *big.Int, data []byte, gas uint64, what string) (*types.Receipt, error) {
|
|
r, err := c.send(from, to, value, data, gas)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", what, err)
|
|
}
|
|
if r.Status != types.ReceiptStatusSuccessful {
|
|
return r, fmt.Errorf("%s reverted (tx %s, block %d)", what, r.TxHash.Hex(), r.BlockNumber.Uint64())
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
func ethCallMsg(to common.Address, data []byte) ethereum.CallMsg {
|
|
return ethereum.CallMsg{To: &to, Data: data}
|
|
}
|
|
|
|
// --- 0x9999 calldata (the one place these are built) ---
|
|
|
|
func withSelector(sel uint32, body []byte) []byte {
|
|
out := make([]byte, 4+len(body))
|
|
out[0], out[1], out[2], out[3] = byte(sel>>24), byte(sel>>16), byte(sel>>8), byte(sel)
|
|
copy(out[4:], body)
|
|
return out
|
|
}
|
|
|
|
func initializeData(key dex.PoolKey, sqrtPriceX96 *big.Int) []byte {
|
|
body := make([]byte, 192)
|
|
copy(body[0:160], dex.EncodePoolKeyABI(key))
|
|
sqrtPriceX96.FillBytes(body[160:192])
|
|
return withSelector(dex.SelectorInitialize, body)
|
|
}
|
|
|
|
func swapData(key dex.PoolKey, zeroForOne bool, amountIn int64, hookData []byte) []byte {
|
|
// ABI: swap((address,address,uint24,int24,address),(bool,int256,uint160),bytes).
|
|
// Head = PoolKey(160) + SwapParams(96) + dynamic-bytes offset(32) = 288.
|
|
body := make([]byte, 288)
|
|
copy(body[0:160], dex.EncodePoolKeyABI(key))
|
|
if zeroForOne {
|
|
body[191] = 1
|
|
}
|
|
// amountSpecified = -amountIn (exact input), two's-complement int256.
|
|
tc := new(big.Int).Add(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(-amountIn))
|
|
tc.FillBytes(body[192:224])
|
|
// sqrtPriceLimitX96 = 0 (no price limit; slippage bounded by the min-out floor).
|
|
new(big.Int).SetUint64(288).FillBytes(body[256:288]) // bytes offset
|
|
hd := make([]byte, 32+((len(hookData)+31)/32)*32)
|
|
new(big.Int).SetUint64(uint64(len(hookData))).FillBytes(hd[0:32])
|
|
copy(hd[32:], hookData)
|
|
body = append(body, hd...)
|
|
return withSelector(dex.SelectorSwap, body)
|
|
}
|
|
|
|
// --- 0x9999 write operations ---
|
|
|
|
// Initialize permissionlessly opens a market (OpenMarket) for key at the given
|
|
// price. sqrtPriceX96 = 1<<96 is parity (1.0). Idempotent at the protocol level: a
|
|
// re-initialize of a bound market is a harmless no-op the caller may ignore.
|
|
func (c *Pool9999) Initialize(from Account, key dex.PoolKey, sqrtPriceX96 *big.Int) (*types.Receipt, error) {
|
|
return c.mustOK(from, &PoolManager, big.NewInt(0), initializeData(key, sqrtPriceX96), 3_000_000, "initialize")
|
|
}
|
|
|
|
// SendNative transfers native LUX (a plain value transfer, no calldata) from
|
|
// `from` to `to` — used to top an ops account's gas from a funded sibling the
|
|
// caller also controls. Real value moves.
|
|
func (c *Pool9999) SendNative(from Account, to common.Address, amount *big.Int) (*types.Receipt, error) {
|
|
return c.mustOK(from, &to, amount, nil, 21000, "sendNative")
|
|
}
|
|
|
|
// Swap takes liquidity (exact input) through the canonical 0x9999 PoolManager.
|
|
// minOut is the required minimum output, carried as canonical hookData (slippage
|
|
// protection).
|
|
func (c *Pool9999) Swap(from Account, key dex.PoolKey, zeroForOne bool, amountIn int64, minOut uint64) (*types.Receipt, error) {
|
|
return c.SwapAt(from, PoolManager, key, zeroForOne, amountIn, minOut)
|
|
}
|
|
|
|
// SwapAt is Swap against an EXPLICIT PoolManager address — the same CLOB swap ABI,
|
|
// but routed to a manager other than the canonical 0x9999 precompile (e.g. a V4
|
|
// PoolManager the migration workstream deploys and publishes in state.json). The
|
|
// canonical Swap delegates here, so there is one swap implementation, one calldata
|
|
// builder — the manager address is the only thing that varies across venues.
|
|
func (c *Pool9999) SwapAt(from Account, manager common.Address, key dex.PoolKey, zeroForOne bool, amountIn int64, minOut uint64) (*types.Receipt, error) {
|
|
return c.mustOK(from, &manager, big.NewInt(0), swapData(key, zeroForOne, amountIn, dex.EncodeMinOutHookData(minOut)), 3_000_000, "swap")
|
|
}
|
|
|
|
// --- 0x9999 read operations (read-only eth_call; no gas, no block) ---
|
|
|
|
// BestBidAsk reads a market's best bid/ask from 0x9999 (getBestBidAsk). Prices are
|
|
// returned as human quote-per-base (the grid integer divided by 1e8); fromBook
|
|
// reports whether the prices came from the live order book (vs a registry fallback).
|
|
func (c *Pool9999) BestBidAsk(poolID common.Hash) (bid, ask float64, fromBook bool, err error) {
|
|
return c.BestBidAskAt(PoolManager, poolID)
|
|
}
|
|
|
|
// BestBidAskAt reads getBestBidAsk from an EXPLICIT PoolManager address (the
|
|
// canonical BestBidAsk delegates here with the 0x9999 precompile). Lets a V4
|
|
// PoolManager published in state.json be read over the identical CLOB read ABI.
|
|
func (c *Pool9999) BestBidAskAt(manager common.Address, poolID common.Hash) (bid, ask float64, fromBook bool, err error) {
|
|
out, err := c.eth.CallContract(c.ctx, ethCallMsg(manager, withSelector(dex.SelectorGetBestBidAsk, poolID.Bytes())), nil)
|
|
if err != nil {
|
|
return 0, 0, false, err
|
|
}
|
|
if len(out) < 96 {
|
|
return 0, 0, false, fmt.Errorf("getBestBidAsk: short return (%d bytes)", len(out))
|
|
}
|
|
bidGrid := new(big.Int).SetBytes(out[0:32])
|
|
askGrid := new(big.Int).SetBytes(out[32:64])
|
|
fromBook = out[95] == 1
|
|
bid = PriceOfBig(bidGrid)
|
|
ask = PriceOfBig(askGrid)
|
|
return bid, ask, fromBook, nil
|
|
}
|
|
|
|
// --- ERC-20 helpers ---
|
|
|
|
//go:embed assets/TestToken.bin
|
|
var testTokenBinHex string
|
|
|
|
// erc20ABI is the minimal ERC-20 surface the client needs (deploy/approve/
|
|
// transfer/balanceOf). The constructor matches the embedded TestToken bytecode.
|
|
const erc20ABI = `[
|
|
{"inputs":[{"name":"_name","type":"string"},{"name":"_symbol","type":"string"},{"name":"_supply","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},
|
|
{"inputs":[{"name":"spender","type":"address"},{"name":"amount","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},
|
|
{"inputs":[{"name":"","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},
|
|
{"inputs":[{"name":"to","type":"address"},{"name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}
|
|
]`
|
|
|
|
// DeployERC20 deploys a real ERC-20 (genuine EXTCODESIZE>0, so the 0x9999 asset
|
|
// resolver admits it) and returns its address.
|
|
func (c *Pool9999) DeployERC20(from Account, name, symbol string, supply *big.Int) (common.Address, error) {
|
|
creation := common.FromHex(strings.TrimSpace(testTokenBinHex))
|
|
args, err := c.erc.Constructor.Inputs.Pack(name, symbol, supply)
|
|
if err != nil {
|
|
return common.Address{}, fmt.Errorf("pack ctor: %w", err)
|
|
}
|
|
r, err := c.mustOK(from, nil, big.NewInt(0), append(creation, args...), 1_500_000, "deploy "+symbol)
|
|
if err != nil {
|
|
return common.Address{}, err
|
|
}
|
|
return r.ContractAddress, nil
|
|
}
|
|
|
|
// ERC20Approve lets spender pull amount of token from the owner.
|
|
func (c *Pool9999) ERC20Approve(owner Account, token, spender common.Address, amount *big.Int) (*types.Receipt, error) {
|
|
data, err := c.erc.Pack("approve", spender, amount)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return c.mustOK(owner, &token, big.NewInt(0), data, 120_000, "approve")
|
|
}
|
|
|
|
// ERC20Transfer moves amount of token from `from` to `to`.
|
|
func (c *Pool9999) ERC20Transfer(from Account, token, to common.Address, amount *big.Int) (*types.Receipt, error) {
|
|
data, err := c.erc.Pack("transfer", to, amount)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return c.mustOK(from, &token, big.NewInt(0), data, 120_000, "transfer")
|
|
}
|
|
|
|
// ERC20Allowance reads allowance(owner, spender) so a caller can skip a redundant
|
|
// approve when the unlimited allowance is already set (idempotent setup).
|
|
func (c *Pool9999) ERC20Allowance(token, owner, spender common.Address) (*big.Int, error) {
|
|
data := append(common.FromHex("0xdd62ed3e"),
|
|
append(common.LeftPadBytes(owner.Bytes(), 32), common.LeftPadBytes(spender.Bytes(), 32)...)...)
|
|
out, err := c.eth.CallContract(c.ctx, ethCallMsg(token, data), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(out) < 32 {
|
|
return new(big.Int), nil
|
|
}
|
|
return new(big.Int).SetBytes(out[:32]), nil
|
|
}
|
|
|
|
// ERC20Decimals reads a token's decimals() and memoizes it. Decimals are immutable
|
|
// for the lifetime of a token, so one read per token per process is exact — and it
|
|
// is the ONE place decimals are learned, so no caller has to be told them. This
|
|
// matters because a market's two legs need not agree: WLUX/USDC is 18/6, WLUX/BTC
|
|
// is 18/8, and scaling both legs by one number silently misprices the pair by
|
|
// twelve orders of magnitude.
|
|
func (c *Pool9999) ERC20Decimals(token common.Address) (int, error) {
|
|
if v, ok := c.decCache.Load(token); ok {
|
|
return v.(int), nil
|
|
}
|
|
out, err := c.eth.CallContract(c.ctx, ethCallMsg(token, sigERC20Decimals), nil)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if len(out) < 32 {
|
|
return 0, fmt.Errorf("decimals(): short result (%d bytes) from %s", len(out), token.Hex())
|
|
}
|
|
d := new(big.Int).SetBytes(out[:32])
|
|
if !d.IsInt64() || d.Int64() < 0 || d.Int64() > 36 {
|
|
return 0, fmt.Errorf("decimals(): implausible value %s from %s", d, token.Hex())
|
|
}
|
|
n := int(d.Int64())
|
|
c.decCache.Store(token, n)
|
|
return n, nil
|
|
}
|
|
|
|
// ERC20DecimalsOr is ERC20Decimals with a fallback for a token whose decimals()
|
|
// cannot be read (a non-standard token). It never fabricates a scale silently: the
|
|
// fallback is the caller's declared default, not a guess made here.
|
|
func (c *Pool9999) ERC20DecimalsOr(token common.Address, fallback int) int {
|
|
if d, err := c.ERC20Decimals(token); err == nil {
|
|
return d
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
// ERC20BalanceOf reads a token balance from accepted state.
|
|
func (c *Pool9999) ERC20BalanceOf(token, who common.Address) (*big.Int, error) {
|
|
data, err := c.erc.Pack("balanceOf", who)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out, err := c.eth.CallContract(c.ctx, ethCallMsg(token, data), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res, err := c.erc.Unpack("balanceOf", out)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return res[0].(*big.Int), nil
|
|
}
|
|
|
|
// --- DEXFill reads ---
|
|
|
|
// DEXFill is one settled DEXFill event on 0x9999 (distinct from the D-Chain venue
|
|
// Fill in dex.go — this is the C-Chain settlement signal).
|
|
type DEXFill struct {
|
|
Block uint64
|
|
Tx common.Hash
|
|
PoolID common.Hash
|
|
Taker common.Address
|
|
AmountOut *big.Int
|
|
}
|
|
|
|
// Fills reads settled DEXFill events from 0x9999 over [fromBlock, latest].
|
|
func (c *Pool9999) Fills(fromBlock uint64) ([]DEXFill, error) {
|
|
logs, err := c.eth.FilterLogs(c.ctx, ethereum.FilterQuery{
|
|
FromBlock: new(big.Int).SetUint64(fromBlock),
|
|
Addresses: []common.Address{PoolManager},
|
|
Topics: [][]common.Hash{{dexFillSig}},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]DEXFill, 0, len(logs))
|
|
for _, lg := range logs {
|
|
if len(lg.Topics) < 3 || len(lg.Data) < 32 {
|
|
continue
|
|
}
|
|
out = append(out, DEXFill{
|
|
Block: lg.BlockNumber,
|
|
Tx: lg.TxHash,
|
|
PoolID: lg.Topics[1],
|
|
Taker: common.BytesToAddress(lg.Topics[2].Bytes()),
|
|
AmountOut: new(big.Int).SetBytes(lg.Data[0:32]),
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// --- pool-key + grid helpers ---
|
|
|
|
// PoolKeyFor builds the canonical V4 pool key for a base/quote pair, ordering the
|
|
// currencies by byte value so currency0 < currency1 (the V4 invariant).
|
|
func PoolKeyFor(base, quote common.Address) dex.PoolKey {
|
|
c0, c1 := base, quote
|
|
if c0.Cmp(c1) > 0 {
|
|
c0, c1 = c1, c0
|
|
}
|
|
return dex.PoolKey{
|
|
Currency0: dex.Currency{Address: c0},
|
|
Currency1: dex.Currency{Address: c1},
|
|
Fee: 3000,
|
|
TickSpacing: 60,
|
|
Hooks: common.Address{},
|
|
}
|
|
}
|
|
|
|
// priceToGrid converts a human price (quote per base) to the integer CLOB grid.
|
|
|
|
// decodeBytes32ArrayU64 decodes an ABI bytes32[] return into the uint64 order ids
|
|
// it left-pads (the low 8 bytes of each 32-byte word).
|
|
func decodeBytes32ArrayU64(out []byte) ([]uint64, error) {
|
|
if len(out) < 64 {
|
|
return nil, nil // empty array (offset+len) or no data
|
|
}
|
|
n := new(big.Int).SetBytes(out[32:64]).Uint64()
|
|
if 64+n*32 > uint64(len(out)) {
|
|
return nil, fmt.Errorf("getOpenOrders: truncated array (n=%d, %d bytes)", n, len(out))
|
|
}
|
|
ids := make([]uint64, 0, n)
|
|
for i := range n {
|
|
word := out[64+i*32 : 64+i*32+32]
|
|
ids = append(ids, new(big.Int).SetBytes(word).Uint64())
|
|
}
|
|
return ids, nil
|
|
}
|