Files
zeekayandHanzo Dev 1ac4c806b2 Go 1.27.1, and the code it can say more simply
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]>
2026-09-16 19:23:15 -07:00

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
}