mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
chore: remove dexkeeper (abandon off-chain keeper/venue model)
The D-Chain (dexvm) is its own chain: trading is its consensus-driven state
transition (matcher-at-Verify in pkg/dchain). There is no off-chain keeper
driving 0x9999 — 0x9999 is only the C<->D settlement adapter. Remove the
keeper and its footprint:
- cmd/dexkeeper/ (the off-chain relayer)
- Dockerfile.dexkeeper
- .github/workflows/docker-dexkeeper.yml + docker.yml reference
- k8s/devnet-keeper/ (never deployed)
- go.mod: drop luxfi/chains dep (was pinned at v1.3.16 ONLY for the keeper;
nothing else in dex imports chains — go mod tidy removes it)
Native dchain VM + matcher build pure-Go (CGO_ENABLED=0) and the full
pkg/dchain suite passes (consensus match->Verify->Accept, conservation,
LUX/LUSD + LUX/LETH venue e2e, ownership red-team).
This commit is contained in:
@@ -1,97 +0,0 @@
|
||||
name: Docker dexkeeper
|
||||
|
||||
# Self-contained image build for ghcr.io/luxfi/dexkeeper on the lux-build ARC
|
||||
# pool. Deliberately INLINE (not the hanzoai/.github reusable workflow): luxfi/*
|
||||
# repos cannot resolve the cross-org reusable `docker-build.yml@main` (GitHub
|
||||
# "workflow was not found" at validation — the reason docker.yml fails at 0s).
|
||||
# This mirrors luxfi/node's proven inline Docker workflow exactly.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
paths:
|
||||
- 'cmd/dexkeeper/**'
|
||||
- 'Dockerfile.dexkeeper'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- '.github/workflows/docker-dexkeeper.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build-amd64:
|
||||
# In-cluster ARC pool (lux-build autoscalingrunnerset in lux-k8s, amd64
|
||||
# DOKS nodes, DinD sidecar) — same pool luxfi/node builds on.
|
||||
runs-on: lux-build
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver: docker-container
|
||||
driver-opts: |
|
||||
network=host
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Image metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/luxfi/dexkeeper
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=sha,format=short,prefix=sha-
|
||||
|
||||
- name: Resolve cross-repo PAT for private luxfi/* deps
|
||||
id: pat
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
UNIVERSE_PAT: ${{ secrets.UNIVERSE_PAT }}
|
||||
run: |
|
||||
tok="$GH_TOKEN"
|
||||
src="GH_TOKEN"
|
||||
if [ -z "$tok" ]; then
|
||||
tok="$UNIVERSE_PAT"
|
||||
src="UNIVERSE_PAT"
|
||||
fi
|
||||
if [ -z "$tok" ]; then
|
||||
echo "::error::Neither GH_TOKEN (org) nor UNIVERSE_PAT (repo) is set. Private luxfi/* deps cannot be resolved."
|
||||
exit 1
|
||||
fi
|
||||
echo "Using secret: $src (length=${#tok})"
|
||||
echo "::add-mask::$tok"
|
||||
echo "token<<EOF" >> "$GITHUB_OUTPUT"
|
||||
echo "$tok" >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build & push dexkeeper (amd64)
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.dexkeeper
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
# The Dockerfile resolves private luxfi/* modules via git url.insteadOf,
|
||||
# taking the token from the BuildKit secret id=ghtok (node's convention).
|
||||
secrets: |
|
||||
ghtok=${{ steps.pat.outputs.token }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
provenance: false
|
||||
cache-from: type=registry,ref=ghcr.io/luxfi/dexkeeper:buildcache-amd64
|
||||
cache-to: type=registry,ref=ghcr.io/luxfi/dexkeeper:buildcache-amd64,mode=max
|
||||
@@ -20,9 +20,6 @@ jobs:
|
||||
dockerfile: Dockerfile
|
||||
- image: ghcr.io/luxfi/dex-gateway
|
||||
dockerfile: Dockerfile.gateway
|
||||
# NOTE: ghcr.io/luxfi/dexkeeper builds via the self-contained
|
||||
# docker-dexkeeper.yml (inline, lux-build pool) — this reusable-workflow
|
||||
# path cannot resolve hanzoai/.github cross-org ("workflow was not found").
|
||||
uses: hanzoai/.github/.github/workflows/docker-build.yml@main
|
||||
with:
|
||||
image: ${{ matrix.image }}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
#
|
||||
# dexkeeper — the off-chain keeper that drives the on-chain 0x9999 two-phase atomic
|
||||
# DEX seam (watch IntentSubmitted -> dexvm ImportTx + RelayOrderTx via dex.submitTx ->
|
||||
# poll dex.getSettlement -> C-side Phase-B settle -> DEXFill). Pure-Go (CGO disabled);
|
||||
# the maker key is derived in-process from the LUX_MNEMONIC env (KMS-synced secret).
|
||||
#
|
||||
# BUILD DEP NOTE: the keeper imports github.com/luxfi/zap (>= v0.8.10, the
|
||||
# EncodeIntentHookData/EncodeSettlementHookData calldata) and github.com/luxfi/chains
|
||||
# (the dexvm txs + the dex.submitTx/getSettlement RPC server). Both tags must be cut
|
||||
# before this image builds from published modules alone (go.work is NOT used in CI).
|
||||
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache git ca-certificates
|
||||
|
||||
ENV GOPRIVATE=github.com/luxfi/*,github.com/hanzoai/*
|
||||
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
# Private cross-repo Go modules (luxfi/*): the docker-dexkeeper workflow exposes
|
||||
# the org PAT as a BuildKit secret id=ghtok (luxfi/node's convention). Mount it
|
||||
# for the module fetch only (never baked into a layer). ARG GITHUB_TOKEN remains
|
||||
# a fallback for local `--build-arg` builds; the secret takes precedence.
|
||||
ARG GITHUB_TOKEN
|
||||
RUN --mount=type=secret,id=ghtok \
|
||||
TOKEN="$(cat /run/secrets/ghtok 2>/dev/null || echo "${GITHUB_TOKEN}")" && \
|
||||
if [ -n "$TOKEN" ]; then \
|
||||
git config --global url."https://${TOKEN}@github.com/".insteadOf "https://github.com/"; \
|
||||
fi && \
|
||||
go mod download
|
||||
COPY . .
|
||||
|
||||
ARG TARGETARCH=amd64
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -ldflags="-s -w" -o dexkeeper ./cmd/dexkeeper
|
||||
|
||||
FROM alpine:3.21
|
||||
|
||||
RUN apk add --no-cache ca-certificates && \
|
||||
addgroup -g 1000 -S keeper && \
|
||||
adduser -u 1000 -S keeper -G keeper
|
||||
|
||||
WORKDIR /home/keeper
|
||||
COPY --from=builder /app/dexkeeper .
|
||||
RUN chown keeper:keeper dexkeeper
|
||||
|
||||
USER keeper
|
||||
|
||||
# No ports: the keeper is an egress-only client (C-Chain RPC + dexvm RPC + venue ZAP).
|
||||
ENTRYPOINT ["./dexkeeper"]
|
||||
@@ -1,195 +0,0 @@
|
||||
// calldata.go holds the PURE wire logic the keeper drives the C<->D seam with:
|
||||
//
|
||||
// - Phase-A / Phase-B 0x9999 swap calldata, built via github.com/luxfi/zap/dexsession
|
||||
// (the canonical reproduction of the precompile's calldata format).
|
||||
// - The intent-id derivation, mirroring precompile/dex DeriveIntentID. The keeper
|
||||
// CHOOSES the nonce when it signs Phase-A, so it derives the id locally; it also
|
||||
// reads the id back from the IntentSubmitted topic to correlate (authoritative).
|
||||
// - Decoders for the two 0x9999 events the keeper observes: IntentSubmitted (the
|
||||
// routing event it acts on) and DEXFill (the terminal settled-fill signal).
|
||||
//
|
||||
// Everything here is a deterministic function of values — no I/O, no chain handle — so
|
||||
// it is unit-testable in isolation (calldata_test.go) and the encode side is asserted
|
||||
// byte-identical to the precompile/dexsession authorities.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
|
||||
"github.com/luxfi/crypto"
|
||||
"github.com/luxfi/dex/pkg/zapwire"
|
||||
"github.com/luxfi/geth/common"
|
||||
ethtypes "github.com/luxfi/geth/core/types"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/zap/dexsession"
|
||||
)
|
||||
|
||||
// poolManagerAddr9999 is the canonical V4-shaped settlement precompile (the money path).
|
||||
// Phase-A intents and Phase-B settlements are both swap calls to this address; the
|
||||
// IntentSubmitted / DEXFill events are emitted from it.
|
||||
var poolManagerAddr9999 = common.HexToAddress("0x0000000000000000000000000000000000009999")
|
||||
|
||||
// nativeIntentDomain scopes the intent-id derivation. IDENTICAL to precompile/dex
|
||||
// nativeIntentDomain — a one-sided change here would make the keeper's locally-derived
|
||||
// id disagree with the chain's (the watch-correlation contract); calldata_test pins it.
|
||||
const nativeIntentDomain = "lux.dex.native.intent.v1"
|
||||
|
||||
// IntentSubmitted(bytes32,bytes32,address,bytes32,uint256,bytes32,uint256,address,uint64,uint8,uint64,uint8)
|
||||
// — the keeper-routing event. topics[1]=intentID, topics[2]=dChainID. Data is 10 ABI
|
||||
// words: account,assetIn,locked,marketID,minAmountOut,recipient,deadline,kind,priceLimit,
|
||||
// limitIsUpper (see precompile/dex native_events.go emitNativeRoutingEvent).
|
||||
var intentSubmittedSig = common.BytesToHash(crypto.Keccak256([]byte(
|
||||
"IntentSubmitted(bytes32,bytes32,address,bytes32,uint256,bytes32,uint256,address,uint64,uint8,uint64,uint8)")))
|
||||
|
||||
// DEXFill(bytes32,address,uint256,uint256) — the terminal settled-fill signal.
|
||||
// topics[1]=poolId, topics[2]=taker. Data is 2 ABI words: amountOut, blockNumber.
|
||||
var dexFillSig = common.BytesToHash(crypto.Keccak256([]byte("DEXFill(bytes32,address,uint256,uint256)")))
|
||||
|
||||
// deriveIntentID re-derives the C->D intent id over the FULL identity, mirroring
|
||||
// precompile/dex DeriveIntentID:
|
||||
//
|
||||
// SHA-256( domain | networkID(4 BE) | cChainID | dChainID |
|
||||
// account | assetIn | amountIn(8 BE) | marketID | nonce(8 BE) )
|
||||
//
|
||||
// The keeper picks the nonce, so it can derive the id BEFORE the Phase-A tx lands. It is
|
||||
// only used to sanity-check the id it reads from the event topic (authoritative).
|
||||
func deriveIntentID(networkID uint32, cChainID, dChainID ids.ID, account common.Address, assetIn [32]byte, amountIn uint64, marketID [32]byte, nonce uint64) ids.ID {
|
||||
h := sha256.New()
|
||||
h.Write([]byte(nativeIntentDomain))
|
||||
var u4 [4]byte
|
||||
binary.BigEndian.PutUint32(u4[:], networkID)
|
||||
h.Write(u4[:])
|
||||
h.Write(cChainID[:])
|
||||
h.Write(dChainID[:])
|
||||
h.Write(account[:])
|
||||
h.Write(assetIn[:])
|
||||
var u8 [8]byte
|
||||
binary.BigEndian.PutUint64(u8[:], amountIn)
|
||||
h.Write(u8[:])
|
||||
h.Write(marketID[:])
|
||||
binary.BigEndian.PutUint64(u8[:], nonce)
|
||||
h.Write(u8[:])
|
||||
var out [32]byte
|
||||
copy(out[:], h.Sum(nil))
|
||||
return ids.ID(out)
|
||||
}
|
||||
|
||||
// phaseAIntentCalldata builds the full 0x9999 Phase-A (DI01) swap calldata: a V4 swap
|
||||
// with an explicit intent hookData carrying (deadline, nonce). The maker signs and sends
|
||||
// this to 0x9999; SubmitSwapIntent locks the input and stages the C->D object.
|
||||
func phaseAIntentCalldata(pk dexsession.PoolKeyArgs, zeroForOne bool, amountIn, deadline, nonce uint64) []byte {
|
||||
return dexsession.EncodeSwapCalldata(pk, zeroForOne, amountIn, dexsession.EncodeIntentHookData(deadline, nonce))
|
||||
}
|
||||
|
||||
// phaseBSettleCalldata builds the full 0x9999 Phase-B (DS01) swap calldata: a V4 swap
|
||||
// with a settlement hookData carrying (outputID, amount, intentID). The maker signs and
|
||||
// sends this; ImportSettlement consumes the D->C proceeds object and emits DEXFill. The
|
||||
// amount/zeroForOne mirror Phase-A so the swap ABI is well-formed; only the hookData
|
||||
// (the phase selector) differs.
|
||||
func phaseBSettleCalldata(pk dexsession.PoolKeyArgs, zeroForOne bool, amountIn uint64, outputID, intentID ids.ID, amount uint64) []byte {
|
||||
body := dexsession.EncodeSettlementHookData(dexsession.ID(outputID), amount, dexsession.ID(intentID))
|
||||
return dexsession.EncodeSwapCalldata(pk, zeroForOne, amountIn, body)
|
||||
}
|
||||
|
||||
// intentEvent is the decoded IntentSubmitted routing payload the keeper acts on.
|
||||
type intentEvent struct {
|
||||
IntentID ids.ID // topics[1]
|
||||
DChainID ids.ID // topics[2]
|
||||
Account common.Address
|
||||
AssetIn [32]byte
|
||||
AmountIn uint64 // the locked principal (observed delta), low 64 bits of the word
|
||||
MarketID [32]byte
|
||||
MinAmountOut *big.Int
|
||||
Recipient common.Address
|
||||
Deadline uint64
|
||||
Kind uint8 // 0 swap, 1 position
|
||||
PriceLimit uint64
|
||||
LimitIsUpper bool
|
||||
}
|
||||
|
||||
// decodeIntentEvent decodes an IntentSubmitted log. It validates the topic[0] signature
|
||||
// and the 10-word (320-byte) data layout at the boundary (untrusted chain input), then
|
||||
// extracts the routing fields. Returns an error on any shape mismatch so a malformed or
|
||||
// foreign log is never acted on.
|
||||
func decodeIntentEvent(log ethtypes.Log) (intentEvent, error) {
|
||||
var e intentEvent
|
||||
if len(log.Topics) != 3 {
|
||||
return e, fmt.Errorf("IntentSubmitted: want 3 topics, got %d", len(log.Topics))
|
||||
}
|
||||
if log.Topics[0] != intentSubmittedSig {
|
||||
return e, fmt.Errorf("IntentSubmitted: topic0 %s != sig %s", log.Topics[0], intentSubmittedSig)
|
||||
}
|
||||
const wantWords = 10
|
||||
if len(log.Data) != wantWords*32 {
|
||||
return e, fmt.Errorf("IntentSubmitted: want %d data bytes, got %d", wantWords*32, len(log.Data))
|
||||
}
|
||||
e.IntentID = ids.ID(log.Topics[1])
|
||||
e.DChainID = ids.ID(log.Topics[2])
|
||||
word := func(i int) []byte { return log.Data[i*32 : i*32+32] }
|
||||
e.Account = common.BytesToAddress(word(0))
|
||||
copy(e.AssetIn[:], word(1))
|
||||
e.AmountIn = beUint64Low(word(2))
|
||||
copy(e.MarketID[:], word(3))
|
||||
e.MinAmountOut = new(big.Int).SetBytes(word(4))
|
||||
e.Recipient = common.BytesToAddress(word(5))
|
||||
e.Deadline = beUint64Low(word(6))
|
||||
e.Kind = word(7)[31]
|
||||
e.PriceLimit = beUint64Low(word(8))
|
||||
e.LimitIsUpper = word(9)[31] != 0
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// dexFillEvent is the decoded DEXFill terminal signal.
|
||||
type dexFillEvent struct {
|
||||
PoolID [32]byte // topics[1]
|
||||
Taker common.Address
|
||||
AmountOut *big.Int
|
||||
BlockNumber uint64
|
||||
}
|
||||
|
||||
// decodeDEXFillEvent decodes a DEXFill log (the proof the C<->D round-trip settled).
|
||||
func decodeDEXFillEvent(log ethtypes.Log) (dexFillEvent, error) {
|
||||
var e dexFillEvent
|
||||
if len(log.Topics) != 3 {
|
||||
return e, fmt.Errorf("DEXFill: want 3 topics, got %d", len(log.Topics))
|
||||
}
|
||||
if log.Topics[0] != dexFillSig {
|
||||
return e, fmt.Errorf("DEXFill: topic0 %s != sig %s", log.Topics[0], dexFillSig)
|
||||
}
|
||||
if len(log.Data) != 2*32 {
|
||||
return e, fmt.Errorf("DEXFill: want 64 data bytes, got %d", len(log.Data))
|
||||
}
|
||||
copy(e.PoolID[:], log.Topics[1].Bytes())
|
||||
e.Taker = common.BytesToAddress(log.Topics[2].Bytes())
|
||||
e.AmountOut = new(big.Int).SetBytes(log.Data[0:32])
|
||||
e.BlockNumber = beUint64Low(log.Data[32:64])
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// priceLimitFloat converts the V4-style price-limit bits the precompile carried in the
|
||||
// event (a CLOB quote-per-base limit as float64 bits) into the float64 limit price the
|
||||
// settling clob_submit frame needs. The precompile already did the SqrtPriceLimitX96 ->
|
||||
// CLOB conversion (priceLimitToCLOB on the C side) and emitted the result, so the keeper
|
||||
// carries it verbatim. Zero bits == 0.0 == "no limit" (a marketable order takes any price).
|
||||
func priceLimitFloat(bits uint64) float64 {
|
||||
return math.Float64frombits(bits)
|
||||
}
|
||||
|
||||
// beUint64Low reads the low 8 bytes of a 32-byte big-endian ABI word as a uint64. The
|
||||
// precompile right-aligns uint64/uint8 values in their words (PutUint64(w[24:32], v)), so
|
||||
// the value lives in the last 8 bytes.
|
||||
func beUint64Low(word []byte) uint64 {
|
||||
return binary.BigEndian.Uint64(word[24:32])
|
||||
}
|
||||
|
||||
// submitFrameFor builds the settling taker clob_submit frame the RelayOrderTx carries:
|
||||
// a marketable order on poolID for `size` at the taker's worst-acceptable `limitPrice`,
|
||||
// keyed to the maker user handle. side/limitPrice come from the intent direction +
|
||||
// carried price limit. Byte-identical to zapwire.EncodeSubmit (the venue wire).
|
||||
func submitFrameFor(poolID [32]byte, side uint8, isMarket bool, limitPrice, size float64, user string) []byte {
|
||||
return zapwire.EncodeSubmit(poolID, side, isMarket, limitPrice, size, user)
|
||||
}
|
||||
@@ -1,551 +0,0 @@
|
||||
// keeper.go is the off-chain KEEPER that drives the on-chain 0x9999 two-phase atomic
|
||||
// DEX seam so a DEXFill lands on the C-Chain. It is a relayer with NO custody authority:
|
||||
// the value is locked in shared memory by the C-side SubmitSwapIntent before the keeper
|
||||
// ever sees it, and the D->C proceeds object pays its RECORDED owner regardless of what
|
||||
// the keeper submits. The worst a faulty/byzantine keeper can do is fail to build a
|
||||
// settlement (which the locker reclaims directly after the intent deadline). See
|
||||
// precompile/dex native_events.go and chains/dexvm txs/tx.go for the authority model.
|
||||
//
|
||||
// The seam, end to end:
|
||||
//
|
||||
// 1. Phase-A intent: the maker signs a V4 swap tx to 0x9999 with DI01 hookData (a
|
||||
// chosen nonce + deadline). SubmitSwapIntent locks the input, stages a C->D atomic
|
||||
// object keyed by intentID, and emits IntentSubmitted.
|
||||
// 2. The keeper reads IntentSubmitted (FilterLogs), then submits to the dexvm:
|
||||
// - an ImportTx that consumes the C->D object and funds the D order, and
|
||||
// - a settling RelayOrderTx (clob_submit) bound to that import's collateral.
|
||||
// Both go to the dexvm's dex.submitTx RPC; the proposer relays the order once and
|
||||
// settleFromFills exports a D->C railSwap proceeds object.
|
||||
// 3. The keeper discovers the D->C export's outputID, then has the maker sign a
|
||||
// Phase-B V4 swap tx to 0x9999 with DS01 hookData (outputID|amount|intentID).
|
||||
// ImportSettlement consumes the proceeds object and emits DEXFill.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
dextxs "github.com/luxfi/chains/dexvm/txs"
|
||||
"github.com/luxfi/dex/pkg/zapwire"
|
||||
"github.com/luxfi/geth/common"
|
||||
ethtypes "github.com/luxfi/geth/core/types"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/zap/dexsession"
|
||||
|
||||
ethereum "github.com/luxfi/geth"
|
||||
"github.com/luxfi/geth/ethclient"
|
||||
)
|
||||
|
||||
// keeperConfig is the fully-resolved runtime config (flags + boot-queried values).
|
||||
type keeperConfig struct {
|
||||
CChainRPC string
|
||||
DRPC string // dexvm RPC base URL (…/ext/bc/<D-chain-id>/rpc)
|
||||
DChainID ids.ID
|
||||
VenueAddr string
|
||||
Maker common.Address
|
||||
ChainID *big.Int // C-Chain EVM chainID (eth_chainId)
|
||||
NetworkID uint32 // Lux primary network id (for intent-id derivation)
|
||||
}
|
||||
|
||||
// keeper holds the live clients + the maker key + resolved config.
|
||||
type keeper struct {
|
||||
cfg keeperConfig
|
||||
cc *ethclient.Client
|
||||
maker *makerKey
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
// newKeeper dials the C-Chain, resolves the chainID/networkID if unset, and binds the
|
||||
// maker key. The dexvm is reached over JSON-RPC by URL (no client object to dial).
|
||||
func newKeeper(ctx context.Context, cfg keeperConfig, mnemonic string) (*keeper, error) {
|
||||
cc, err := ethclient.DialContext(ctx, cfg.CChainRPC)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dial C-Chain %s: %w", cfg.CChainRPC, err)
|
||||
}
|
||||
if cfg.ChainID == nil {
|
||||
id, err := cc.ChainID(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("eth_chainId: %w", err)
|
||||
}
|
||||
cfg.ChainID = id
|
||||
}
|
||||
if cfg.NetworkID == 0 {
|
||||
// The Lux primary network id is the C-Chain EVM chainID for sovereign L1s, and
|
||||
// for the primary network it is the convention-fixed id. The intent-id derivation
|
||||
// needs the value the precompile used (atomicState.NetworkID()); on devnet the
|
||||
// C-Chain EVM chainID equals the network id, so we default to it. Override with
|
||||
// --network-id if a deployment separates them.
|
||||
cfg.NetworkID = uint32(cfg.ChainID.Uint64())
|
||||
}
|
||||
mk, err := deriveMakerKey(mnemonic, cfg.Maker, 1000)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("derive maker key: %w", err)
|
||||
}
|
||||
return &keeper{cfg: cfg, cc: cc, maker: mk, http: &http.Client{Timeout: 30 * time.Second}}, nil
|
||||
}
|
||||
|
||||
// cChainID reads the C-Chain's blockchain id (the 32-byte chain id the precompile uses in
|
||||
// DeriveIntentID, NOT the EVM chainID). On the primary network the C-Chain blockchain id
|
||||
// is a fixed alias; the keeper reads it from the node so it matches the chain exactly.
|
||||
//
|
||||
// NOTE: the geth ethclient does not expose the platform blockchain id. The authoritative
|
||||
// value rides the IntentSubmitted correlation anyway (the keeper acts on the id in the
|
||||
// event topic, not a re-derived one), so cChainID is only needed for the optional local
|
||||
// re-derivation sanity check. It is resolved lazily from the configured value when set.
|
||||
func (k *keeper) cChainBlockchainID() ids.ID {
|
||||
// The C-Chain blockchain id is not queryable via eth RPC; the keeper correlates on the
|
||||
// event's intentID topic (authoritative) rather than re-deriving, so an empty value
|
||||
// here only disables the optional local-derivation cross-check.
|
||||
return ids.Empty
|
||||
}
|
||||
|
||||
// ---- Phase A: send one intent swap from the maker ----
|
||||
|
||||
// sendPhaseAIntent builds, signs, and sends ONE Phase-A (DI01) swap to 0x9999 from the
|
||||
// maker. It returns the locally-derived intentID (correlation hint) and the C tx hash.
|
||||
// The keeper CHOOSES the nonce; SubmitSwapIntent re-derives the SAME id from the same
|
||||
// calldata, so the IntentSubmitted topic will carry this id.
|
||||
func (k *keeper) sendPhaseAIntent(ctx context.Context, pk dexsession.PoolKeyArgs, marketID, assetIn [32]byte, zeroForOne bool, amountIn, nonce, deadline uint64) (ids.ID, common.Hash, error) {
|
||||
data := phaseAIntentCalldata(pk, zeroForOne, amountIn, deadline, nonce)
|
||||
txHash, err := k.sendSwapTx(ctx, data)
|
||||
if err != nil {
|
||||
return ids.Empty, common.Hash{}, fmt.Errorf("send Phase-A swap: %w", err)
|
||||
}
|
||||
// Local id derivation is a correlation hint only; the chain's id (read from the
|
||||
// IntentSubmitted topic) is authoritative. cChainBlockchainID() is Empty over eth RPC,
|
||||
// so this matches the chain only when both sides use the same (here it documents the
|
||||
// derivation; the watch loop trusts the topic).
|
||||
intentID := deriveIntentID(k.cfg.NetworkID, k.cChainBlockchainID(), k.cfg.DChainID, k.maker.Address, assetIn, amountIn, marketID, nonce)
|
||||
return intentID, txHash, nil
|
||||
}
|
||||
|
||||
// sendSwapTx signs a legacy EVM tx to 0x9999 carrying `data` with the maker key and
|
||||
// submits it via eth_sendRawTransaction. It uses a suggested gas price + the maker's
|
||||
// pending nonce; the swap precompile is gas-metered, so a generous limit is used.
|
||||
func (k *keeper) sendSwapTx(ctx context.Context, data []byte) (common.Hash, error) {
|
||||
nonce, err := k.cc.PendingNonceAt(ctx, k.maker.Address)
|
||||
if err != nil {
|
||||
return common.Hash{}, fmt.Errorf("pending nonce: %w", err)
|
||||
}
|
||||
gasPrice, err := k.cc.SuggestGasPrice(ctx)
|
||||
if err != nil {
|
||||
return common.Hash{}, fmt.Errorf("suggest gas price: %w", err)
|
||||
}
|
||||
to := poolManagerAddr9999
|
||||
inner := ðtypes.LegacyTx{
|
||||
Nonce: nonce,
|
||||
GasPrice: gasPrice,
|
||||
Gas: 600_000,
|
||||
To: &to,
|
||||
Value: big.NewInt(0),
|
||||
Data: data,
|
||||
}
|
||||
signed, err := ethtypes.SignTx(ethtypes.NewTx(inner), ethtypes.LatestSignerForChainID(k.cfg.ChainID), k.maker.ecdsa)
|
||||
if err != nil {
|
||||
return common.Hash{}, fmt.Errorf("sign tx: %w", err)
|
||||
}
|
||||
if err := k.cc.SendTransaction(ctx, signed); err != nil {
|
||||
return common.Hash{}, fmt.Errorf("send raw tx: %w", err)
|
||||
}
|
||||
return signed.Hash(), nil
|
||||
}
|
||||
|
||||
// ---- The watch loop ----
|
||||
|
||||
// watch runs the keeper loop: poll the C-Chain for new IntentSubmitted logs from 0x9999,
|
||||
// and for each previously-unseen intent, drive it through import+relay (and Phase-B once
|
||||
// export discovery is wired). It is restart-safe: it tracks the last scanned block and the
|
||||
// set of handled intent ids so a re-seen log is not re-driven.
|
||||
func (k *keeper) watch(ctx context.Context, fromBlock uint64) error {
|
||||
handled := make(map[ids.ID]bool)
|
||||
next := fromBlock
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
log.Info("keeper watch loop started", "fromBlock", fromBlock, "maker", k.cfg.Maker, "dChainID", k.cfg.DChainID)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
}
|
||||
head, err := k.cc.BlockNumber(ctx)
|
||||
if err != nil {
|
||||
log.Warn("BlockNumber", "err", err)
|
||||
continue
|
||||
}
|
||||
if head < next {
|
||||
continue
|
||||
}
|
||||
logs, err := k.intentLogs(ctx, next, head)
|
||||
if err != nil {
|
||||
log.Warn("FilterLogs IntentSubmitted", "from", next, "to", head, "err", err)
|
||||
continue
|
||||
}
|
||||
for _, lg := range logs {
|
||||
ev, err := decodeIntentEvent(lg)
|
||||
if err != nil {
|
||||
log.Warn("decode IntentSubmitted", "err", err)
|
||||
continue
|
||||
}
|
||||
if handled[ev.IntentID] {
|
||||
continue
|
||||
}
|
||||
if err := k.handleIntent(ctx, ev); err != nil {
|
||||
log.Error("handle intent", "intentID", ev.IntentID, "err", err)
|
||||
continue // leave unhandled so a later pass can retry
|
||||
}
|
||||
handled[ev.IntentID] = true
|
||||
}
|
||||
next = head + 1
|
||||
}
|
||||
}
|
||||
|
||||
// intentLogs queries 0x9999 for IntentSubmitted logs in [from, to].
|
||||
func (k *keeper) intentLogs(ctx context.Context, from, to uint64) ([]ethtypes.Log, error) {
|
||||
q := ethereum.FilterQuery{
|
||||
FromBlock: new(big.Int).SetUint64(from),
|
||||
ToBlock: new(big.Int).SetUint64(to),
|
||||
Addresses: []common.Address{poolManagerAddr9999},
|
||||
Topics: [][]common.Hash{{intentSubmittedSig}},
|
||||
}
|
||||
return k.cc.FilterLogs(ctx, q)
|
||||
}
|
||||
|
||||
// handleIntent drives ONE intent through the D-side legs: build+submit the ImportTx
|
||||
// (consume the C->D object, fund the D order) and the settling RelayOrderTx (clob_submit),
|
||||
// then discover the D->C export and build Phase-B. Only swap intents (kind 0) are driven
|
||||
// here; position intents (kind 1) are routed by a different op and are skipped.
|
||||
func (k *keeper) handleIntent(ctx context.Context, ev intentEvent) error {
|
||||
if ev.Kind != intentKindSwap {
|
||||
log.Info("skip non-swap intent", "intentID", ev.IntentID, "kind", ev.Kind)
|
||||
return nil
|
||||
}
|
||||
log.Info("handling swap intent", "intentID", ev.IntentID, "account", ev.Account,
|
||||
"assetIn", hex.EncodeToString(ev.AssetIn[:]), "amountIn", ev.AmountIn, "marketID", hex.EncodeToString(ev.MarketID[:]))
|
||||
|
||||
// 1) ImportTx: consume the C->D object (keyed by intentID) and credit the D order's
|
||||
// funding to the maker. The import's single input names the staged object (UTXOID =
|
||||
// intentID), its asset (assetIn) and locked amount; the output credits the same to
|
||||
// the maker on the swap rail.
|
||||
owner := ids.ShortID(k.maker.Address)
|
||||
importTx := dextxs.NewImportTx(
|
||||
owner,
|
||||
ev.AmountIn, // nonce: any monotone value; the import is content-addressed by its inputs
|
||||
k.cChainSourceID(),
|
||||
[]dextxs.AtomicInput{{UTXOID: ev.IntentID, Asset: ids.ID(ev.AssetIn), Amount: ev.AmountIn}},
|
||||
[]dextxs.AtomicOutput{{Rail: dextxs.RailSwap, Owner: owner, Asset: ids.ID(ev.AssetIn), Amount: ev.AmountIn}},
|
||||
)
|
||||
importID, err := k.submitDexTx(ctx, importTx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("submit ImportTx: %w", err)
|
||||
}
|
||||
log.Info("submitted ImportTx", "txID", importID, "collateralRef", importTx.ID())
|
||||
|
||||
// 2) Settling RelayOrderTx: the taker clob_submit frame, bound to the import's
|
||||
// collateral (CollateralRef = importTx.ID()). The order is a marketable BUY (the
|
||||
// taker spends assetIn to receive the opposite side) at the carried slippage limit.
|
||||
// assetOut is the opposite side of the market — the keeper knows the market mapping;
|
||||
// here it is carried as the market's quote/base counterpart of assetIn.
|
||||
//
|
||||
// The relay is UNSIGNED, with From set to the maker's STANDARD EVM address (==
|
||||
// the escrow owner the precompile recorded). This is the architecturally-correct
|
||||
// build: settleFromFills derives the settle authority + payout target from the
|
||||
// consumed C->D object's RECORDED owner (escrowOwner), and REQUIRES the relay's
|
||||
// From to equal it (relaySender != escrowOwner -> errSettleUnauthorized); From
|
||||
// carries no independent authority, so an unsigned relay is admissible and cannot
|
||||
// escalate (chains/dexvm txs/tx.go RelayOrderTx.Verify / vm.go settleFromFills).
|
||||
// We deliberately do NOT call relay.Sign(secp): that would overwrite From with
|
||||
// secp256k1.PrivateKey.EVMAddress(), which hashes the COMPRESSED pubkey and so
|
||||
// yields a NON-standard address that does NOT equal the standard escrow owner —
|
||||
// the signed relay would be refused with errSettleUnauthorized. See the keeper
|
||||
// report's "secp256k1.EVMAddress divergence" blocker.
|
||||
side := zapwire.SideBuy
|
||||
limitPrice := priceLimitFloat(ev.PriceLimit)
|
||||
size := float64(ev.AmountIn)
|
||||
frame := submitFrameFor(poolIDFromMarket(ev.MarketID), side, true /*isMarket*/, limitPrice, size, makerHandle(k.cfg.Maker))
|
||||
assetOut := k.assetOutFor(ev.MarketID, ev.AssetIn)
|
||||
// CollateralRef MUST be the C->D object's UTXOID (== ev.IntentID), NOT importTx.ID():
|
||||
// executeImport keys the escrow by tx.ImportedInputs[0].UTXOID (atomic.go), and
|
||||
// settleFromFills looks the escrow up by RelayOrderTx.CollateralRef. They MUST be the
|
||||
// same id or the settle finds no escrow (proceeds-only, no refund) and the authority
|
||||
// bind degrades. The import's single input is ev.IntentID, so that is the collateral ref.
|
||||
collateralRef := ev.IntentID
|
||||
relay := dextxs.NewSettlingRelayOrderTx(owner, ev.AmountIn+1, frame, collateralRef, assetOut, ev.PriceLimit, ev.LimitIsUpper)
|
||||
relayID, err := k.submitDexTx(ctx, relay)
|
||||
if err != nil {
|
||||
return fmt.Errorf("submit RelayOrderTx: %w", err)
|
||||
}
|
||||
log.Info("submitted settling RelayOrderTx (unsigned; From=escrow owner)", "txID", relayID)
|
||||
|
||||
_ = relayID
|
||||
// 3) Poll the dexvm for the D->C proceeds object's coordinate (dex.getSettlement),
|
||||
// keyed by the collateral ref (== ev.IntentID). Once the proposer relays and the
|
||||
// settling block accepts, settleFromFills records (outputID, amount); the keeper
|
||||
// reads them and builds Phase-B.
|
||||
outputID, amountOut, err := k.getExportedOutput(ctx, collateralRef)
|
||||
if err != nil {
|
||||
return fmt.Errorf("discover D->C export (intent driven through relay; Phase-B pending): %w", err)
|
||||
}
|
||||
return k.sendPhaseBSettle(ctx, ev, outputID, amountOut)
|
||||
}
|
||||
|
||||
// sendPhaseBSettle has the maker sign+send the Phase-B (DS01) swap to 0x9999, consuming
|
||||
// the discovered D->C proceeds object. ImportSettlement credits the maker and emits DEXFill.
|
||||
func (k *keeper) sendPhaseBSettle(ctx context.Context, ev intentEvent, outputID ids.ID, amountOut uint64) error {
|
||||
pk := poolKeyForMarket(ev.MarketID)
|
||||
// zeroForOne mirrors the Phase-A direction; amountIn keeps the swap ABI well-formed.
|
||||
data := phaseBSettleCalldata(pk, true, ev.AmountIn, outputID, ev.IntentID, amountOut)
|
||||
txHash, err := k.sendSwapTx(ctx, data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("send Phase-B swap: %w", err)
|
||||
}
|
||||
log.Info("submitted Phase-B settlement", "intentID", ev.IntentID, "outputID", outputID, "amountOut", amountOut, "txHash", txHash)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- dexvm tx submission over dex.submitTx ----
|
||||
|
||||
// submitTxRequest / submitTxResponse mirror the gorilla-rpc envelope the dexvm's
|
||||
// dex.submitTx serves (chains/dexvm/api/service.go SubmitTxArgs/SubmitTxReply).
|
||||
type submitTxRequest struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID int `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Params [1]submitTxParams `json:"params"`
|
||||
}
|
||||
|
||||
type submitTxParams struct {
|
||||
Tx string `json:"tx"` // hex-encoded tx wire bytes
|
||||
}
|
||||
|
||||
type submitTxResponse struct {
|
||||
Result *struct {
|
||||
TxID string `json:"txID"`
|
||||
} `json:"result"`
|
||||
Error *struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
// submitDexTx hex-encodes a dexvm tx's wire bytes and POSTs them to dex.submitTx, returning
|
||||
// the dexvm's tx id. The wire is the dexvm codec (type byte + JSON), exactly tx.Bytes().
|
||||
func (k *keeper) submitDexTx(ctx context.Context, tx dextxs.Tx) (string, error) {
|
||||
body, err := json.Marshal(submitTxRequest{
|
||||
JSONRPC: "2.0",
|
||||
ID: 1,
|
||||
Method: "dex.submitTx",
|
||||
Params: [1]submitTxParams{{Tx: hex.EncodeToString(tx.Bytes())}},
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal submitTx request: %w", err)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, k.cfg.DRPC, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("new request: %w", err)
|
||||
}
|
||||
req.Header.Set("content-type", "application/json")
|
||||
resp, err := k.http.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("POST dex.submitTx: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var out submitTxResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
||||
return "", fmt.Errorf("decode submitTx response: %w", err)
|
||||
}
|
||||
if out.Error != nil {
|
||||
return "", fmt.Errorf("dex.submitTx error %d: %s", out.Error.Code, out.Error.Message)
|
||||
}
|
||||
if out.Result == nil {
|
||||
return "", fmt.Errorf("dex.submitTx: empty result")
|
||||
}
|
||||
return out.Result.TxID, nil
|
||||
}
|
||||
|
||||
// ---- export discovery (the one documented stub) ----
|
||||
|
||||
// getExportedOutput discovers the D->C railSwap proceeds object that settleFromFills
|
||||
// exported for THIS intent, returning its shared-memory outputID and credited amount so
|
||||
// the keeper can build the Phase-B settlement.
|
||||
//
|
||||
// WHY THIS IS A STUB (and exactly what must fill it): the export UTXO key is
|
||||
//
|
||||
// deriveUTXOID( settlementExportTx.ID(), outputIndex )
|
||||
//
|
||||
// where (chains/dexvm vm.go settleFromFills + NewSettlementExportTx):
|
||||
//
|
||||
// settlementExportTx = NewSettlementExportTx(
|
||||
// taker = ids.ShortID(maker EVM address),
|
||||
// txIndex = the relay's index within the D settlement block,
|
||||
// destChain = cChainID,
|
||||
// outs = [ proceeds(RailSwap, owner=taker, asset=assetOut, amount=Σfill_out, spent=Σfill_in),
|
||||
// refund(RailSwap, owner=taker, asset=assetIn, amount=locked-spent) ], // refund omitted if 0
|
||||
// fillRef = collateralRef (== importTx.ID()),
|
||||
// createdAt = int64(BigEndian.Uint64(blockHash[:8])), // D settlement block hash
|
||||
// )
|
||||
//
|
||||
// and the proceeds leg is outputIndex 0 (nonZeroOutputs preserves proceeds-before-refund
|
||||
// order). Everything EXCEPT (blockHash, txIndex) and the realized fill amounts is known to
|
||||
// the keeper. (blockHash, txIndex, fills) are properties of the D block that settled the
|
||||
// relay — and the dexvm RPC currently exposes only {Ping, Status, Relay, SubmitTx}
|
||||
// (chains/dexvm/api/service.go), none of which return them. The C-Chain EVM does not
|
||||
// expose an atomic-getUTXOs for the dexvm seam either (verified in luxfi/node plugin/evm).
|
||||
//
|
||||
// TO COMPLETE, add ONE of:
|
||||
// - a dexvm RPC `dex.getSettlement(relayTxID)` returning {blockHash, txIndex, fills[]}
|
||||
// (or {outputID, amount} directly), then recompute the key here with deriveUTXOID; or
|
||||
// - a C-Chain view that lists importable D->C railSwap UTXOs for an address+sourceChain,
|
||||
// and select the one whose recorded (owner==maker, asset==assetOut) matches this intent.
|
||||
//
|
||||
// IT IS NOW WIRED via dex.getSettlement(collateralRef): settleFromFills records the
|
||||
// proceeds export's (outputID, amount) under the collateral ref (== ev.IntentID) in the
|
||||
// versiondb when the settling block accepts (chains/dexvm state.PutSettlement /
|
||||
// vm.GetSettlement / api dex.getSettlement). The keeper polls it: until the proposer
|
||||
// relays + the block accepts, Settled is false (keep polling); once true, the
|
||||
// (outputID, amount) are the Phase-B DS01 body inputs.
|
||||
const (
|
||||
settlePollInterval = 1 * time.Second
|
||||
settlePollMax = 60 // ~60s: the proposer relays at the next BuildBlock and settles at accept
|
||||
)
|
||||
|
||||
// getSettlementRequest / getSettlementResponse mirror the gorilla-rpc envelope of the
|
||||
// dexvm's dex.getSettlement (chains/dexvm/api/service.go GetSettlementArgs/Reply).
|
||||
type getSettlementRequest struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID int `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Params [1]getSettlementParams `json:"params"`
|
||||
}
|
||||
|
||||
type getSettlementParams struct {
|
||||
CollateralRef string `json:"collateralRef"`
|
||||
}
|
||||
|
||||
type getSettlementResponse struct {
|
||||
Result *struct {
|
||||
Settled bool `json:"settled"`
|
||||
OutputID string `json:"outputID"`
|
||||
Amount uint64 `json:"amount"`
|
||||
} `json:"result"`
|
||||
Error *struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
// getExportedOutput polls dex.getSettlement(collateralRef) until the dexvm has settled the
|
||||
// relay and recorded the proceeds object's coordinate, returning its outputID + amount for
|
||||
// Phase-B. collateralRef is ev.IntentID (the escrow + settlement key).
|
||||
func (k *keeper) getExportedOutput(ctx context.Context, collateralRef ids.ID) (outputID ids.ID, amount uint64, err error) {
|
||||
body, merr := json.Marshal(getSettlementRequest{
|
||||
JSONRPC: "2.0",
|
||||
ID: 1,
|
||||
Method: "dex.getSettlement",
|
||||
Params: [1]getSettlementParams{{CollateralRef: collateralRef.String()}},
|
||||
})
|
||||
if merr != nil {
|
||||
return ids.Empty, 0, fmt.Errorf("marshal getSettlement request: %w", merr)
|
||||
}
|
||||
for attempt := 0; attempt < settlePollMax; attempt++ {
|
||||
req, rerr := http.NewRequestWithContext(ctx, http.MethodPost, k.cfg.DRPC, bytes.NewReader(body))
|
||||
if rerr != nil {
|
||||
return ids.Empty, 0, fmt.Errorf("new getSettlement request: %w", rerr)
|
||||
}
|
||||
req.Header.Set("content-type", "application/json")
|
||||
resp, derr := k.http.Do(req)
|
||||
if derr != nil {
|
||||
return ids.Empty, 0, fmt.Errorf("POST dex.getSettlement: %w", derr)
|
||||
}
|
||||
var out getSettlementResponse
|
||||
decErr := json.NewDecoder(resp.Body).Decode(&out)
|
||||
resp.Body.Close()
|
||||
if decErr != nil {
|
||||
return ids.Empty, 0, fmt.Errorf("decode getSettlement response: %w", decErr)
|
||||
}
|
||||
if out.Error != nil {
|
||||
return ids.Empty, 0, fmt.Errorf("dex.getSettlement error %d: %s", out.Error.Code, out.Error.Message)
|
||||
}
|
||||
if out.Result != nil && out.Result.Settled {
|
||||
oid, perr := ids.FromString(out.Result.OutputID)
|
||||
if perr != nil {
|
||||
return ids.Empty, 0, fmt.Errorf("getSettlement returned bad outputID %q: %w", out.Result.OutputID, perr)
|
||||
}
|
||||
return oid, out.Result.Amount, nil
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ids.Empty, 0, ctx.Err()
|
||||
case <-time.After(settlePollInterval):
|
||||
}
|
||||
}
|
||||
return ids.Empty, 0, fmt.Errorf("dex.getSettlement: no proceeds recorded for %s after %d polls (relay may have zero-filled; taker can reclaim after deadline)", collateralRef, settlePollMax)
|
||||
}
|
||||
|
||||
// ---- market mapping helpers ----
|
||||
//
|
||||
// The keeper binds a D market id to its (poolID, base/quote assets, V4 PoolKey). On a
|
||||
// single-market devnet proof the mapping is the identity poolID==marketID with the venue's
|
||||
// base/quote; a multi-market deployment supplies a real registry. These keep the mapping in
|
||||
// ONE place so the rest of the keeper is market-agnostic.
|
||||
|
||||
// poolIDFromMarket maps a marketID to the venue poolID. The seam keys both by the same
|
||||
// 32-byte id (the D market IS the venue book), so this is the identity.
|
||||
func poolIDFromMarket(marketID [32]byte) [32]byte { return marketID }
|
||||
|
||||
// poolKeyForMarket builds the V4 PoolKey the 0x9999 calldata needs for a market. The
|
||||
// native seam matches by marketID, not by the PoolKey tuple, so the keeper passes a
|
||||
// well-formed key whose hooks point at 0x9999; currency0/1 are the market's assets' EVM
|
||||
// addresses (address(0) for native), fee/tickSpacing the market's V4 params.
|
||||
func poolKeyForMarket(marketID [32]byte) dexsession.PoolKeyArgs {
|
||||
return dexsession.PoolKeyArgs{
|
||||
Hooks: dexsession.Account(poolManagerAddr9999),
|
||||
}
|
||||
}
|
||||
|
||||
// assetOutFor returns the opposite side of a market from assetIn (the asset a taker BUY
|
||||
// receives). On the single-market devnet proof the market's quote and base are the two
|
||||
// venue assets; the keeper subtracts assetIn from the pair. A real registry supplies this.
|
||||
func (k *keeper) assetOutFor(marketID, assetIn [32]byte) ids.ID {
|
||||
// Identity-market devnet default: the output asset is carried by the seed spec and
|
||||
// equals the market's base when assetIn is the quote (and vice-versa). With no registry
|
||||
// here, return the marketID-derived counterpart; the dexvm equality-rejects a wrong
|
||||
// asset (taker-liveness only, no theft), so this is the bounded surface documented on
|
||||
// RelayOrderTx.AssetOut.
|
||||
_ = assetIn
|
||||
return ids.ID(marketID)
|
||||
}
|
||||
|
||||
// makerHandle is the venue user handle for the maker, derived from its EVM address so the
|
||||
// custody rows are stable and addressable. zapwire user width is 16 bytes; the handle is
|
||||
// the address hex truncated to fit.
|
||||
func makerHandle(addr common.Address) string {
|
||||
h := addr.Hex() // 0x + 40 hex
|
||||
if len(h) > zapwire.UserSize {
|
||||
return h[:zapwire.UserSize]
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// cChainSourceID is the source chain the ImportTx names (the chain the C->D object was
|
||||
// exported from — the C-Chain). It equals the configured C-Chain blockchain id; on the
|
||||
// primary network the dexvm resolves it as the "C" alias. When the keeper has no platform
|
||||
// blockchain id (eth RPC does not expose it), the dexvm validates the import against the
|
||||
// recorded object regardless, so the configured D-chain peering is authoritative.
|
||||
func (k *keeper) cChainSourceID() ids.ID {
|
||||
// The import's SourceChain must equal the C-Chain blockchain id the object was PUT from.
|
||||
// It is not queryable over eth RPC; the dexvm binds the import to the recorded object's
|
||||
// source. Carried as Empty here documents the dependency; a deployment that needs an
|
||||
// explicit value supplies it via the (future) settlement query or a --c-blockchain-id.
|
||||
return k.cChainBlockchainID()
|
||||
}
|
||||
|
||||
// intentKindSwap mirrors precompile/dex nativeIntentKind: 0 = taker swap, 1 = LP position.
|
||||
const intentKindSwap uint8 = 0
|
||||
@@ -1,368 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/crypto"
|
||||
"github.com/luxfi/genesis/pkg/genesis"
|
||||
"github.com/luxfi/geth/common"
|
||||
ethtypes "github.com/luxfi/geth/core/types"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/zap/dexsession"
|
||||
)
|
||||
|
||||
// ---- maker-key derivation ----
|
||||
|
||||
// TestDeriveMakerKeyMatchesGenesisAuthority pins the keeper's BIP44 walk against the
|
||||
// canonical genesis derivation (genesis.LoadBIP44WalletKeysFromMnemonic). A one-sided
|
||||
// drift from m/44'/9000'/0'/0/i — different coin type, hardening, or hashing — changes
|
||||
// the derived address and trips this test BEFORE the keeper ever signs with a wrong key.
|
||||
func TestDeriveMakerKeyMatchesGenesisAuthority(t *testing.T) {
|
||||
const scan = 8
|
||||
authority, err := genesis.LoadBIP44WalletKeysFromMnemonic(genesis.LightMnemonic, scan)
|
||||
if err != nil {
|
||||
t.Fatalf("genesis.LoadBIP44WalletKeysFromMnemonic: %v", err)
|
||||
}
|
||||
for i, want := range authority {
|
||||
wantAddr := common.BytesToAddress(want.EVMAddr[:])
|
||||
got, err := deriveMakerKey(genesis.LightMnemonic, wantAddr, scan)
|
||||
if err != nil {
|
||||
t.Fatalf("deriveMakerKey index %d (%s): %v", i, wantAddr, err)
|
||||
}
|
||||
if got.Address != wantAddr {
|
||||
t.Fatalf("index %d: derived %s, genesis authority %s", i, got.Address, wantAddr)
|
||||
}
|
||||
if got.Index != i {
|
||||
t.Fatalf("index %d: keeper found it at index %d", i, got.Index)
|
||||
}
|
||||
// The ecdsa signing key must re-derive the SAME standard address (the C-Chain
|
||||
// caller / escrow-owner format keccak256(X||Y)[12:]).
|
||||
if a := evmAddress(got.ecdsa); a != wantAddr {
|
||||
t.Fatalf("index %d: ecdsa key address %s != genesis authority %s", i, a, wantAddr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeriveMakerKeyKnownVector pins index 0 under the public LightMnemonic against a
|
||||
// hardcoded address, so the derivation is anchored to a fixed value (not just to the
|
||||
// authority, which could co-drift). This is the canonical BIP44 m/44'/9000'/0'/0/0 key.
|
||||
func TestDeriveMakerKeyKnownVector(t *testing.T) {
|
||||
const wantHex = "0x5369615110ca435bdf798f31c20ba6163d7b0a54"
|
||||
want := common.HexToAddress(wantHex)
|
||||
got, err := deriveMakerKey(genesis.LightMnemonic, want, 4)
|
||||
if err != nil {
|
||||
t.Fatalf("deriveMakerKey: %v", err)
|
||||
}
|
||||
if got.Address != want {
|
||||
t.Fatalf("index 0 derived %s, want %s", got.Address, want)
|
||||
}
|
||||
if got.Index != 0 {
|
||||
t.Fatalf("known vector is index 0, keeper found %d", got.Index)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeriveMakerKeyNotFound asserts a clean error when the wanted address is not in the
|
||||
// scanned range (rather than a silent wrong key).
|
||||
func TestDeriveMakerKeyNotFound(t *testing.T) {
|
||||
absent := common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
|
||||
if _, err := deriveMakerKey(genesis.LightMnemonic, absent, 4); err == nil {
|
||||
t.Fatal("expected error for absent address, got nil")
|
||||
}
|
||||
if _, err := deriveMakerKey("not a valid mnemonic", absent, 4); err == nil {
|
||||
t.Fatal("expected error for invalid mnemonic, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Phase-A / Phase-B calldata ----
|
||||
|
||||
// TestPhaseAIntentCalldata asserts the keeper's Phase-A builder is byte-identical to a
|
||||
// direct dexsession.EncodeSwapCalldata with a DI01 intent hookData, and that the calldata
|
||||
// has the expected structural shape: 0xF3CD914C selector, 9 head words, then a DI01-tagged
|
||||
// hookData carrying the deadline + nonce.
|
||||
func TestPhaseAIntentCalldata(t *testing.T) {
|
||||
pk := dexsession.PoolKeyArgs{Hooks: dexsession.Account(poolManagerAddr9999), Fee: 3000, TickSpacing: 60}
|
||||
const (
|
||||
amountIn = uint64(5)
|
||||
deadline = uint64(1_700_000_000)
|
||||
nonce = uint64(42)
|
||||
)
|
||||
got := phaseAIntentCalldata(pk, false, amountIn, deadline, nonce)
|
||||
want := dexsession.EncodeSwapCalldata(pk, false, amountIn, dexsession.EncodeIntentHookData(deadline, nonce))
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatalf("phaseAIntentCalldata != dexsession.EncodeSwapCalldata\n got=%x\nwant=%x", got, want)
|
||||
}
|
||||
// Selector 0xF3CD914C (dexsession.SelectorSwap).
|
||||
if sel := binary.BigEndian.Uint32(got[0:4]); sel != dexsession.SelectorSwap {
|
||||
t.Fatalf("selector %#x, want %#x", sel, dexsession.SelectorSwap)
|
||||
}
|
||||
// Head: selector(4) + 9 words. The dynamic hookData tail begins after the head and
|
||||
// carries the DI01 tag.
|
||||
const headLen = 4 + 9*32
|
||||
if len(got) <= headLen {
|
||||
t.Fatalf("calldata len %d, want > head %d (a hookData tail)", len(got), headLen)
|
||||
}
|
||||
// The hookData length word is the last head word's pointee: the tail's first 32 bytes
|
||||
// are the length, then the DI01-tagged bytes.
|
||||
tail := got[headLen:]
|
||||
hookLen := binary.BigEndian.Uint64(tail[24:32])
|
||||
hookData := tail[32 : 32+hookLen]
|
||||
if string(hookData[:4]) != "DI01" {
|
||||
t.Fatalf("hookData tag %q, want DI01", hookData[:4])
|
||||
}
|
||||
// DI01 + deadline[32] + nonce[32] for a nonzero deadline+nonce.
|
||||
if hookLen != 4+64 {
|
||||
t.Fatalf("hookData len %d, want %d (tag + deadline + nonce)", hookLen, 4+64)
|
||||
}
|
||||
if d := binary.BigEndian.Uint64(hookData[4+24 : 4+32]); d != deadline {
|
||||
t.Fatalf("hookData deadline %d, want %d", d, deadline)
|
||||
}
|
||||
if n := binary.BigEndian.Uint64(hookData[4+32+24 : 4+32+32]); n != nonce {
|
||||
t.Fatalf("hookData nonce %d, want %d", n, nonce)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPhaseBSettleCalldata asserts the Phase-B builder carries a DS01 settlement hookData
|
||||
// with the outputID|amount|intentID body (the intentID word is what binds the credit to
|
||||
// the taker's intent record; a missing one is the wrong width and the precompile reverts).
|
||||
func TestPhaseBSettleCalldata(t *testing.T) {
|
||||
pk := dexsession.PoolKeyArgs{Hooks: dexsession.Account(poolManagerAddr9999)}
|
||||
outputID := ids.ID{0x01, 0x02, 0x03}
|
||||
intentID := ids.ID{0xaa, 0xbb, 0xcc}
|
||||
const (
|
||||
amountIn = uint64(7)
|
||||
amountOut = uint64(123)
|
||||
)
|
||||
got := phaseBSettleCalldata(pk, true, amountIn, outputID, intentID, amountOut)
|
||||
body := dexsession.EncodeSettlementHookData(dexsession.ID(outputID), amountOut, dexsession.ID(intentID))
|
||||
want := dexsession.EncodeSwapCalldata(pk, true, amountIn, body)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatalf("phaseBSettleCalldata != expected\n got=%x\nwant=%x", got, want)
|
||||
}
|
||||
const headLen = 4 + 9*32
|
||||
tail := got[headLen:]
|
||||
hookLen := binary.BigEndian.Uint64(tail[24:32])
|
||||
hookData := tail[32 : 32+hookLen]
|
||||
if string(hookData[:4]) != "DS01" {
|
||||
t.Fatalf("hookData tag %q, want DS01", hookData[:4])
|
||||
}
|
||||
// DS01 + outputID(32) + amount(32) + intentID(32) = 4 + 96.
|
||||
if hookLen != 4+96 {
|
||||
t.Fatalf("DS01 body len %d, want %d (tag + outputID + amount + intentID)", hookLen, 4+96)
|
||||
}
|
||||
if !bytes.Equal(hookData[4:36], outputID[:]) {
|
||||
t.Fatalf("DS01 outputID %x, want %x", hookData[4:36], outputID[:])
|
||||
}
|
||||
if a := binary.BigEndian.Uint64(hookData[36+24 : 36+32]); a != amountOut {
|
||||
t.Fatalf("DS01 amount %d, want %d", a, amountOut)
|
||||
}
|
||||
if !bytes.Equal(hookData[68:100], intentID[:]) {
|
||||
t.Fatalf("DS01 intentID %x, want %x", hookData[68:100], intentID[:])
|
||||
}
|
||||
}
|
||||
|
||||
// ---- intent-id derivation ----
|
||||
|
||||
// TestDeriveIntentIDDeterministicAndInjective asserts the id is a pure function of its
|
||||
// inputs (same inputs -> same id) and injective over the disambiguating axes (a changed
|
||||
// nonce / account / amount changes the id). This mirrors precompile/dex DeriveIntentID;
|
||||
// the keeper derives the SAME id the chain does from the same calldata (watch correlation).
|
||||
func TestDeriveIntentIDDeterministicAndInjective(t *testing.T) {
|
||||
net := uint32(96369)
|
||||
cID := ids.ID{0xc0}
|
||||
dID := ids.ID{0xd0}
|
||||
acct := common.HexToAddress("0x9011E888251AB053B7bD1cdB598Db4f9DEd94714")
|
||||
asset := [32]byte{0x4c, 0x55, 0x58}
|
||||
market := [32]byte{0x4d}
|
||||
const amount, nonce = uint64(10), uint64(1)
|
||||
|
||||
base := deriveIntentID(net, cID, dID, acct, asset, amount, market, nonce)
|
||||
if base != deriveIntentID(net, cID, dID, acct, asset, amount, market, nonce) {
|
||||
t.Fatal("deriveIntentID not deterministic")
|
||||
}
|
||||
if base == (ids.ID{}) {
|
||||
t.Fatal("deriveIntentID returned empty id")
|
||||
}
|
||||
// Each axis must change the id.
|
||||
cases := []struct {
|
||||
name string
|
||||
id ids.ID
|
||||
}{
|
||||
{"nonce", deriveIntentID(net, cID, dID, acct, asset, amount, market, nonce+1)},
|
||||
{"amount", deriveIntentID(net, cID, dID, acct, asset, amount+1, market, nonce)},
|
||||
{"account", deriveIntentID(net, cID, dID, common.Address{0x01}, asset, amount, market, nonce)},
|
||||
{"network", deriveIntentID(net+1, cID, dID, acct, asset, amount, market, nonce)},
|
||||
{"market", deriveIntentID(net, cID, dID, acct, asset, amount, [32]byte{0x4e}, nonce)},
|
||||
{"dChain", deriveIntentID(net, cID, ids.ID{0xd1}, acct, asset, amount, market, nonce)},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if c.id == base {
|
||||
t.Fatalf("changing %s did not change the intent id", c.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- IntentSubmitted event decode ----
|
||||
|
||||
// makeIntentLog builds a synthetic IntentSubmitted log with the exact wire the precompile
|
||||
// emits (3 topics, 10 ABI data words) so the decoder can be exercised without a chain.
|
||||
func makeIntentLog(e intentEvent) ethtypes.Log {
|
||||
data := make([]byte, 0, 10*32)
|
||||
word := func(b []byte) { var w [32]byte; copy(w[32-len(b):], b); data = append(data, w[:]...) }
|
||||
addrWord := func(a common.Address) { word(a.Bytes()) }
|
||||
u64Word := func(v uint64) { var b [8]byte; binary.BigEndian.PutUint64(b[:], v); word(b[:]) }
|
||||
|
||||
addrWord(e.Account) // 0 account
|
||||
data = append(data, e.AssetIn[:]...) // 1 assetIn (full 32)
|
||||
u64Word(e.AmountIn) // 2 locked
|
||||
data = append(data, e.MarketID[:]...) // 3 marketID
|
||||
min := e.MinAmountOut
|
||||
if min == nil {
|
||||
min = big.NewInt(0)
|
||||
}
|
||||
word(min.Bytes()) // 4 minAmountOut
|
||||
addrWord(e.Recipient) // 5 recipient
|
||||
u64Word(e.Deadline) // 6 deadline
|
||||
u64Word(uint64(e.Kind)) // 7 kind
|
||||
u64Word(e.PriceLimit) // 8 priceLimit
|
||||
var limitFlag uint64
|
||||
if e.LimitIsUpper {
|
||||
limitFlag = 1
|
||||
}
|
||||
u64Word(limitFlag) // 9 limitIsUpper
|
||||
|
||||
return ethtypes.Log{
|
||||
Address: poolManagerAddr9999,
|
||||
Topics: []common.Hash{
|
||||
intentSubmittedSig,
|
||||
common.BytesToHash(e.IntentID[:]),
|
||||
common.BytesToHash(e.DChainID[:]),
|
||||
},
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeIntentEventRoundTrip(t *testing.T) {
|
||||
want := intentEvent{
|
||||
IntentID: ids.ID{0x11, 0x22, 0x33},
|
||||
DChainID: ids.ID{0xd0, 0xd1},
|
||||
Account: common.HexToAddress("0x9011E888251AB053B7bD1cdB598Db4f9DEd94714"),
|
||||
AssetIn: [32]byte{0x4c, 0x55, 0x53, 0x44},
|
||||
AmountIn: 250,
|
||||
MarketID: [32]byte{0x4d, 0x4b, 0x54},
|
||||
MinAmountOut: big.NewInt(99),
|
||||
Recipient: common.HexToAddress("0x5369615110ca435bdf798f31c20ba6163d7b0a54"),
|
||||
Deadline: 1_700_000_000,
|
||||
Kind: intentKindSwap,
|
||||
PriceLimit: 0x4034000000000000, // float64 bits of 20.0
|
||||
LimitIsUpper: true,
|
||||
}
|
||||
got, err := decodeIntentEvent(makeIntentLog(want))
|
||||
if err != nil {
|
||||
t.Fatalf("decodeIntentEvent: %v", err)
|
||||
}
|
||||
if got.IntentID != want.IntentID || got.DChainID != want.DChainID {
|
||||
t.Fatalf("ids: got (%s,%s) want (%s,%s)", got.IntentID, got.DChainID, want.IntentID, want.DChainID)
|
||||
}
|
||||
if got.Account != want.Account || got.Recipient != want.Recipient {
|
||||
t.Fatalf("addrs: got (%s,%s) want (%s,%s)", got.Account, got.Recipient, want.Account, want.Recipient)
|
||||
}
|
||||
if got.AssetIn != want.AssetIn || got.MarketID != want.MarketID {
|
||||
t.Fatalf("asset/market mismatch")
|
||||
}
|
||||
if got.AmountIn != want.AmountIn || got.Deadline != want.Deadline || got.Kind != want.Kind {
|
||||
t.Fatalf("amount/deadline/kind: got (%d,%d,%d) want (%d,%d,%d)", got.AmountIn, got.Deadline, got.Kind, want.AmountIn, want.Deadline, want.Kind)
|
||||
}
|
||||
if got.PriceLimit != want.PriceLimit || got.LimitIsUpper != want.LimitIsUpper {
|
||||
t.Fatalf("priceLimit/isUpper: got (%d,%v) want (%d,%v)", got.PriceLimit, got.LimitIsUpper, want.PriceLimit, want.LimitIsUpper)
|
||||
}
|
||||
if got.MinAmountOut.Cmp(want.MinAmountOut) != 0 {
|
||||
t.Fatalf("minAmountOut: got %s want %s", got.MinAmountOut, want.MinAmountOut)
|
||||
}
|
||||
// priceLimitFloat must decode the bits to the float the venue frame expects.
|
||||
if pf := priceLimitFloat(got.PriceLimit); pf != 20.0 {
|
||||
t.Fatalf("priceLimitFloat(%#x) = %v, want 20.0", got.PriceLimit, pf)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDecodeIntentEventRejectsMalformed asserts the boundary decoder fails closed on a
|
||||
// foreign topic0, a wrong topic count, and a wrong data length — never silently acts on a
|
||||
// log that is not an IntentSubmitted of the right shape.
|
||||
func TestDecodeIntentEventRejectsMalformed(t *testing.T) {
|
||||
good := makeIntentLog(intentEvent{IntentID: ids.ID{0x01}, DChainID: ids.ID{0x02}, MinAmountOut: big.NewInt(0)})
|
||||
|
||||
// foreign topic0
|
||||
bad := good
|
||||
bad.Topics = append([]common.Hash{}, good.Topics...)
|
||||
bad.Topics[0] = common.HexToHash("0xdeadbeef")
|
||||
if _, err := decodeIntentEvent(bad); err == nil {
|
||||
t.Fatal("expected error for foreign topic0")
|
||||
}
|
||||
// wrong topic count
|
||||
bad = good
|
||||
bad.Topics = good.Topics[:2]
|
||||
if _, err := decodeIntentEvent(bad); err == nil {
|
||||
t.Fatal("expected error for wrong topic count")
|
||||
}
|
||||
// wrong data length
|
||||
bad = good
|
||||
bad.Data = good.Data[:len(good.Data)-1]
|
||||
if _, err := decodeIntentEvent(bad); err == nil {
|
||||
t.Fatal("expected error for short data")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDecodeDEXFillEvent asserts the terminal DEXFill decode round-trips topics+data.
|
||||
func TestDecodeDEXFillEvent(t *testing.T) {
|
||||
poolID := [32]byte{0xD0, 0x01}
|
||||
taker := common.HexToAddress("0x9011E888251AB053B7bD1cdB598Db4f9DEd94714")
|
||||
amountOut := big.NewInt(4321)
|
||||
blockNumber := uint64(123456)
|
||||
|
||||
data := make([]byte, 0, 64)
|
||||
data = append(data, common.BigToHash(amountOut).Bytes()...)
|
||||
data = append(data, common.BigToHash(new(big.Int).SetUint64(blockNumber)).Bytes()...)
|
||||
log := ethtypes.Log{
|
||||
Address: poolManagerAddr9999,
|
||||
Topics: []common.Hash{
|
||||
dexFillSig,
|
||||
common.BytesToHash(poolID[:]),
|
||||
common.BytesToHash(taker.Bytes()),
|
||||
},
|
||||
Data: data,
|
||||
}
|
||||
got, err := decodeDEXFillEvent(log)
|
||||
if err != nil {
|
||||
t.Fatalf("decodeDEXFillEvent: %v", err)
|
||||
}
|
||||
if got.PoolID != poolID {
|
||||
t.Fatalf("poolID %x, want %x", got.PoolID, poolID)
|
||||
}
|
||||
if got.Taker != taker {
|
||||
t.Fatalf("taker %s, want %s", got.Taker, taker)
|
||||
}
|
||||
if got.AmountOut.Cmp(amountOut) != 0 {
|
||||
t.Fatalf("amountOut %s, want %s", got.AmountOut, amountOut)
|
||||
}
|
||||
if got.BlockNumber != blockNumber {
|
||||
t.Fatalf("blockNumber %d, want %d", got.BlockNumber, blockNumber)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- event signature pinning ----
|
||||
|
||||
// TestEventSignatures pins the two event topic0 hashes against keccak256 of the canonical
|
||||
// ABI signature strings, so a drift from the precompile's emitted signature is caught here.
|
||||
func TestEventSignatures(t *testing.T) {
|
||||
wantIntent := common.BytesToHash(crypto.Keccak256([]byte(
|
||||
"IntentSubmitted(bytes32,bytes32,address,bytes32,uint256,bytes32,uint256,address,uint64,uint8,uint64,uint8)")))
|
||||
if intentSubmittedSig != wantIntent {
|
||||
t.Fatalf("intentSubmittedSig %s != %s", intentSubmittedSig, wantIntent)
|
||||
}
|
||||
wantFill := common.BytesToHash(crypto.Keccak256([]byte("DEXFill(bytes32,address,uint256,uint256)")))
|
||||
if dexFillSig != wantFill {
|
||||
t.Fatalf("dexFillSig %s != %s", dexFillSig, wantFill)
|
||||
}
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
// Command dexkeeper is the off-chain KEEPER that drives the on-chain 0x9999 two-phase
|
||||
// atomic DEX seam so a DEXFill lands on the C-Chain.
|
||||
//
|
||||
// It watches the C-Chain for IntentSubmitted events emitted by the 0x9999 settlement
|
||||
// precompile (Phase-A intent: a maker swap with DI01 hookData), and for each intent
|
||||
// submits — to the dexvm's dex.submitTx RPC — an ImportTx (consume the staged C->D atomic
|
||||
// object, fund the D order) plus a settling RelayOrderTx (clob_submit, collateral-bound).
|
||||
// The dexvm proposer relays the order once and exports a D->C proceeds object; the keeper
|
||||
// then drives the Phase-B settlement swap (DS01 hookData) that consumes it and emits
|
||||
// DEXFill. The keeper holds NO custody authority — value is locked in shared memory by C
|
||||
// before the keeper sees it, and the D->C object pays its recorded owner regardless.
|
||||
//
|
||||
// Modes:
|
||||
// - seed : seed maker liquidity on the venue (one resting ask) and exit.
|
||||
// - phase-a : seed liquidity + send ONE Phase-A intent swap from the maker; print the
|
||||
// derived intentID + C tx hash and exit. (The watch loop, run separately or
|
||||
// via -mode watch, then drives it to settlement.)
|
||||
// - watch : run the keeper loop, watching IntentSubmitted and driving each intent.
|
||||
//
|
||||
// The maker key is derived from the BIP-39 mnemonic in LUX_MNEMONIC at m/44'/9000'/0'/0/i,
|
||||
// scanning for the index whose EVM address equals -maker (a KMS-synced secret supplies the
|
||||
// mnemonic at deploy).
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// LUX_MNEMONIC="..." dexkeeper \
|
||||
// -mode watch \
|
||||
// -c-rpc https://api.lux-dev.network/ext/bc/C/rpc \
|
||||
// -d-rpc http://luxd-0.luxd-headless.lux-devnet.svc.cluster.local:9650/ext/bc/<D>/rpc \
|
||||
// -d-chain-id <D> \
|
||||
// -venue dchain-venue.lux-devnet.svc.cluster.local:9099 \
|
||||
// -maker 0x9011E888251AB053B7bD1cdB598Db4f9DEd94714
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/genesis/pkg/genesis"
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
)
|
||||
|
||||
// defaultMaker is the devnet treasury / maker address (genesis.TreasuryAddress).
|
||||
const defaultMaker = "0x9011E888251AB053B7bD1cdB598Db4f9DEd94714"
|
||||
|
||||
func main() {
|
||||
var (
|
||||
mode = flag.String("mode", "watch", "watch | phase-a | seed")
|
||||
cRPC = flag.String("c-rpc", "", "C-Chain EVM JSON-RPC (e.g. https://api.lux-dev.network/ext/bc/C/rpc)")
|
||||
dRPC = flag.String("d-rpc", "", "dexvm RPC base URL (…/ext/bc/<D-chain-id>/rpc)")
|
||||
dChainID = flag.String("d-chain-id", "", "D-Chain (dexvm) blockchain id (CB58)")
|
||||
venueAddr = flag.String("venue", "", "D-Chain venue ZAP address (host:port) for seeding maker liquidity")
|
||||
makerHex = flag.String("maker", defaultMaker, "maker EVM address to derive from LUX_MNEMONIC")
|
||||
chainID = flag.Int64("chain-id", 0, "C-Chain EVM chainID (queried via eth_chainId if 0)")
|
||||
networkID = flag.Uint("network-id", 0, "Lux primary network id for intent-id derivation (defaults to chain-id)")
|
||||
symbol = flag.String("symbol", "LUX/LUSD", "market symbol to seed/route (seed + phase-a modes)")
|
||||
amountIn = flag.Uint64("amount-in", 1, "Phase-A taker input amount (integer asset units)")
|
||||
askPrice = flag.Float64("ask-price", 10.0, "maker ask price to seed (quote per base)")
|
||||
askSize = flag.Float64("ask-size", 1.0, "maker ask size to seed (base units)")
|
||||
fromBlock = flag.Uint64("from-block", 0, "watch: first C-Chain block to scan (0 = current head)")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
mnemonic := os.Getenv(genesis.MnemonicEnvVar)
|
||||
if mnemonic == "" {
|
||||
fatalf("%s not set (the maker key is derived from it; a KMS-synced secret supplies it at deploy)", genesis.MnemonicEnvVar)
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
switch *mode {
|
||||
case "seed":
|
||||
runSeed(ctx, *venueAddr, *symbol, *askPrice, *askSize)
|
||||
case "phase-a":
|
||||
runPhaseA(ctx, mnemonic, keeperFlags(cRPC, dRPC, dChainID, venueAddr, makerHex, chainID, networkID), *venueAddr, *symbol, *amountIn, *askPrice, *askSize)
|
||||
case "watch":
|
||||
runWatch(ctx, mnemonic, keeperFlags(cRPC, dRPC, dChainID, venueAddr, makerHex, chainID, networkID), *fromBlock)
|
||||
default:
|
||||
fatalf("unknown -mode %q (want watch | phase-a | seed)", *mode)
|
||||
}
|
||||
}
|
||||
|
||||
// keeperFlags assembles the resolved keeperConfig from the raw flags, validating the
|
||||
// required ones. ChainID stays nil when -chain-id is 0 so newKeeper queries eth_chainId.
|
||||
func keeperFlags(cRPC, dRPC, dChainID, venueAddr, makerHex *string, chainID *int64, networkID *uint) keeperConfig {
|
||||
if *cRPC == "" {
|
||||
fatalf("-c-rpc required")
|
||||
}
|
||||
if *dRPC == "" {
|
||||
fatalf("-d-rpc required")
|
||||
}
|
||||
if *dChainID == "" {
|
||||
fatalf("-d-chain-id required")
|
||||
}
|
||||
dID, err := ids.FromString(*dChainID)
|
||||
if err != nil {
|
||||
fatalf("invalid -d-chain-id %q: %v", *dChainID, err)
|
||||
}
|
||||
cfg := keeperConfig{
|
||||
CChainRPC: *cRPC,
|
||||
DRPC: *dRPC,
|
||||
DChainID: dID,
|
||||
VenueAddr: *venueAddr,
|
||||
Maker: mustAddress(*makerHex),
|
||||
NetworkID: uint32(*networkID),
|
||||
}
|
||||
if *chainID != 0 {
|
||||
cfg.ChainID = big.NewInt(*chainID)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// runSeed seeds one maker book on the venue and exits.
|
||||
func runSeed(ctx context.Context, venueAddr, symbol string, askPrice, askSize float64) {
|
||||
if venueAddr == "" {
|
||||
fatalf("-venue required for seed mode")
|
||||
}
|
||||
cli, closeCli, err := dialVenue(ctx, venueAddr)
|
||||
if err != nil {
|
||||
fatalf("dial venue: %v", err)
|
||||
}
|
||||
defer closeCli()
|
||||
spec := seedSpecFor(symbol, "mk", askPrice, askSize)
|
||||
pool, err := cli.seedMakerLiquidity(spec)
|
||||
if err != nil {
|
||||
fatalf("seed maker liquidity: %v", err)
|
||||
}
|
||||
log.Info("seeded maker liquidity", "symbol", symbol, "pool", fmt.Sprintf("%x", pool),
|
||||
"maker", spec.Maker, "ask", fmt.Sprintf("%g@%g", askSize, askPrice))
|
||||
}
|
||||
|
||||
// runPhaseA seeds liquidity, then sends ONE Phase-A intent swap from the maker and prints
|
||||
// the derived intentID + C tx hash.
|
||||
func runPhaseA(ctx context.Context, mnemonic string, cfg keeperConfig, venueAddr, symbol string, amountIn uint64, askPrice, askSize float64) {
|
||||
if venueAddr == "" {
|
||||
fatalf("-venue required for phase-a mode (to seed the book the intent fills against)")
|
||||
}
|
||||
cli, closeCli, err := dialVenue(ctx, venueAddr)
|
||||
if err != nil {
|
||||
fatalf("dial venue: %v", err)
|
||||
}
|
||||
spec := seedSpecFor(symbol, "mk", askPrice, askSize)
|
||||
if _, err := cli.seedMakerLiquidity(spec); err != nil {
|
||||
closeCli()
|
||||
fatalf("seed maker liquidity: %v", err)
|
||||
}
|
||||
closeCli()
|
||||
|
||||
k, err := newKeeper(ctx, cfg, mnemonic)
|
||||
if err != nil {
|
||||
fatalf("new keeper: %v", err)
|
||||
}
|
||||
log.Info("maker key derived", "address", k.maker.Address, "index", k.maker.Index, "chainID", k.cfg.ChainID)
|
||||
|
||||
pool := poolIDForSymbol(symbol)
|
||||
marketID := pool // identity market on the devnet proof
|
||||
assetIn := spec.Quote
|
||||
pk := poolKeyForMarket(marketID)
|
||||
nonce := uint64(time.Now().UnixNano())
|
||||
deadline := uint64(time.Now().Add(1 * time.Hour).Unix())
|
||||
|
||||
intentID, txHash, err := k.sendPhaseAIntent(ctx, pk, marketID, assetIn, false /*zeroForOne*/, amountIn, nonce, deadline)
|
||||
if err != nil {
|
||||
fatalf("send Phase-A intent: %v", err)
|
||||
}
|
||||
log.Info("Phase-A intent sent",
|
||||
"intentID(derived)", intentID,
|
||||
"txHash", txHash,
|
||||
"nonce", nonce,
|
||||
"amountIn", amountIn,
|
||||
"market", fmt.Sprintf("%x", marketID))
|
||||
fmt.Printf("PHASE-A: intentID(derived)=%s txHash=%s nonce=%d\n", intentID, txHash.Hex(), nonce)
|
||||
}
|
||||
|
||||
// runWatch runs the keeper loop. fromBlock 0 resolves to the current head at boot.
|
||||
func runWatch(ctx context.Context, mnemonic string, cfg keeperConfig, fromBlock uint64) {
|
||||
k, err := newKeeper(ctx, cfg, mnemonic)
|
||||
if err != nil {
|
||||
fatalf("new keeper: %v", err)
|
||||
}
|
||||
log.Info("maker key derived", "address", k.maker.Address, "index", k.maker.Index, "chainID", k.cfg.ChainID)
|
||||
if fromBlock == 0 {
|
||||
head, err := k.cc.BlockNumber(ctx)
|
||||
if err != nil {
|
||||
fatalf("BlockNumber: %v", err)
|
||||
}
|
||||
fromBlock = head
|
||||
}
|
||||
if err := k.watch(ctx, fromBlock); err != nil && ctx.Err() == nil {
|
||||
fatalf("watch loop: %v", err)
|
||||
}
|
||||
log.Info("keeper stopped")
|
||||
}
|
||||
|
||||
// seedSpecFor builds a single-ask seed spec for a symbol. base/quote follow the venue
|
||||
// convention used by cmd/fourpath-live (a "LUX"-tagged base, a "LUSD"-tagged quote), so a
|
||||
// keeper-seeded book is compatible with the proven live-venue client.
|
||||
func seedSpecFor(symbol, makerPrefix string, askPrice, askSize float64) seedSpec {
|
||||
return seedSpec{
|
||||
Symbol: symbol,
|
||||
Base: assetID(0x4c5558), // "LUX"
|
||||
Quote: assetID(0x4c55534400), // "LUSD"
|
||||
Maker: makerPrefix,
|
||||
AskSize: askSize,
|
||||
AskPrice: askPrice,
|
||||
}
|
||||
}
|
||||
|
||||
// mustAddress parses a 0x-prefixed 20-byte EVM address or exits. The maker flag is an
|
||||
// operator-supplied boundary value, so it is validated here rather than trusted.
|
||||
func mustAddress(s string) common.Address {
|
||||
if !common.IsHexAddress(s) {
|
||||
fatalf("invalid -maker address %q (want 0x + 40 hex)", s)
|
||||
}
|
||||
return common.HexToAddress(s)
|
||||
}
|
||||
|
||||
func fatalf(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, "dexkeeper: "+format+"\n", args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
// maker.go derives the keeper's maker signing key from a BIP-39 mnemonic on the
|
||||
// canonical Lux BIP44 path m/44'/9000'/0'/0/i — the SAME path the genesis builder
|
||||
// (genesis.LoadBIP44WalletKeysFromMnemonic), `lux key derive`, MetaMask and the Lux
|
||||
// wallet use, so a key derived here has the identical EVM address everywhere.
|
||||
//
|
||||
// WHY REPRODUCE THE WALK INSTEAD OF IMPORTING genesis: genesis/pkg/genesis pulls the
|
||||
// full validator-identity graph (ML-DSA-65, BLS, luxfi/tls) to derive NODE identities.
|
||||
// The keeper needs only a user SPENDING key (secp256k1 -> EVM address). This is a small,
|
||||
// frozen, standard BIP44 walk over the SAME primitives (go-bip39 + go-bip32 + secp256k1
|
||||
// + keccak) the genesis builder uses; maker_test pins the derived address against the
|
||||
// known treasury address so a one-sided drift from the canonical path trips a red test.
|
||||
// (Same "three homes" reproduction discipline dexsession uses for the 0x9999 calldata.)
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/crypto"
|
||||
"github.com/luxfi/crypto/keccak256"
|
||||
"github.com/luxfi/geth/common"
|
||||
bip32 "github.com/luxfi/go-bip32"
|
||||
bip39 "github.com/luxfi/go-bip39"
|
||||
)
|
||||
|
||||
// luxCoinType is the BIP44 coin type Lux uses by canonical convention (the 9000' in
|
||||
// m/44'/9000'/0'/0/i). Every BIP44-conformant wallet using that coin type derives the
|
||||
// same key set from the same mnemonic.
|
||||
const luxCoinType = 9000
|
||||
|
||||
// makerKey is a single derived maker identity: the STANDARD EVM address (keccak256(X||Y)
|
||||
// [12:], the C tx-caller / escrow-owner format) plus the ecdsa signing key the 0x9999
|
||||
// C-Chain Phase-A/B swaps are geth-signed with.
|
||||
//
|
||||
// There is intentionally NO secp256k1.PrivateKey wrapper here: the dexvm settling
|
||||
// RelayOrderTx is sent UNSIGNED (its From is the escrow owner — see keeper.go), so the
|
||||
// keeper never invokes secp256k1.PrivateKey.EVMAddress(), which is NON-standard (it hashes
|
||||
// the COMPRESSED pubkey, giving an address that does not equal the standard escrow owner).
|
||||
type makerKey struct {
|
||||
Index int
|
||||
Address common.Address
|
||||
// ecdsa is the standard ecdsa.PrivateKey used by geth types.SignTx for the C-Chain
|
||||
// Phase-A (intent) and Phase-B (settlement) swap transactions.
|
||||
ecdsa *ecdsa.PrivateKey
|
||||
}
|
||||
|
||||
// deriveMakerKey scans m/44'/9000'/0'/0/i for i in [0, scan) and returns the first key
|
||||
// whose EVM address equals want. scan bounds the search (the canonical funded set is the
|
||||
// first 1000 wallet keys). A nil want is invalid — the caller always knows the maker
|
||||
// address it is driving.
|
||||
func deriveMakerKey(mnemonic string, want common.Address, scan int) (*makerKey, error) {
|
||||
if !bip39.IsMnemonicValid(mnemonic) {
|
||||
return nil, fmt.Errorf("invalid mnemonic")
|
||||
}
|
||||
change, err := bip44ChangeNode(mnemonic)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := 0; i < scan; i++ {
|
||||
child, err := change.NewChildKey(uint32(i)) //nolint:gosec // i < scan, bounded
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("derive child %d: %w", i, err)
|
||||
}
|
||||
mk, err := makerKeyFromBytes(child.Key, i)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("key %d: %w", i, err)
|
||||
}
|
||||
if mk.Address == want {
|
||||
return mk, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no key in m/44'/%d'/0'/0/0..%d derives address %s", luxCoinType, scan, want)
|
||||
}
|
||||
|
||||
// bip44ChangeNode walks the hardened prefix m/44'/9000'/0'/0 once; the caller then
|
||||
// derives the non-hardened account index i off the returned change node. This is the
|
||||
// EXACT prefix genesis.LoadBIP44WalletKeysFromMnemonic walks.
|
||||
func bip44ChangeNode(mnemonic string) (*bip32.Key, error) {
|
||||
seed := bip39.NewSeed(mnemonic, "")
|
||||
master, err := bip32.NewMasterKey(seed)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("master key: %w", err)
|
||||
}
|
||||
purpose, err := master.NewChildKey(bip32.FirstHardenedChild + 44)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("derive purpose 44': %w", err)
|
||||
}
|
||||
coin, err := purpose.NewChildKey(bip32.FirstHardenedChild + luxCoinType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("derive coin %d': %w", luxCoinType, err)
|
||||
}
|
||||
account, err := coin.NewChildKey(bip32.FirstHardenedChild + 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("derive account 0': %w", err)
|
||||
}
|
||||
change, err := account.NewChildKey(0) // non-hardened external chain
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("derive change 0: %w", err)
|
||||
}
|
||||
return change, nil
|
||||
}
|
||||
|
||||
// makerKeyFromBytes builds the ecdsa key + standard EVM address from raw 32-byte private
|
||||
// key material. The EVM address is keccak256(pubX||pubY)[12:] — identical to
|
||||
// genesis.keccakAddr and to geth crypto.PubkeyToAddress (the escrow-owner format).
|
||||
func makerKeyFromBytes(raw []byte, index int) (*makerKey, error) {
|
||||
ec, err := crypto.ToECDSA(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ecdsa: %w", err)
|
||||
}
|
||||
return &makerKey{
|
||||
Index: index,
|
||||
Address: evmAddress(ec),
|
||||
ecdsa: ec,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// evmAddress derives the 20-byte EVM address from an ecdsa public key as
|
||||
// keccak256(X||Y)[12:]. Matches genesis.keccakAddr byte-for-byte (the funded-address
|
||||
// contract) — both fill X and Y big-endian into a 64-byte buffer before hashing.
|
||||
func evmAddress(key *ecdsa.PrivateKey) common.Address {
|
||||
buf := make([]byte, 64)
|
||||
key.PublicKey.X.FillBytes(buf[:32])
|
||||
key.PublicKey.Y.FillBytes(buf[32:])
|
||||
h := keccak256.Sum(buf)
|
||||
var a common.Address
|
||||
copy(a[:], h[12:])
|
||||
return a
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
// venue.go is the keeper's direct ZAP client to the live D-Chain venue (cmd/dvenue),
|
||||
// used to SEED maker liquidity so a Phase-A taker order routed through the seam has a
|
||||
// resting book to fill against. It reuses the exact clob_* wire + custody-seal polling
|
||||
// patterns from cmd/fourpath-live (the proven live-venue client): ensure/open a market,
|
||||
// deposit maker inventory, rest a maker ask, and observe balances/depth until sealed.
|
||||
//
|
||||
// This is ONLY the seeding side. The taker order itself does NOT go to the venue directly
|
||||
// from the keeper — it rides the C->D atomic seam: a Phase-A swap to 0x9999 stages a C->D
|
||||
// object, the keeper funds the D order via ImportTx + a settling RelayOrderTx submitted to
|
||||
// the dexvm (keeper.go), and the dexvm proposer relays that order to THIS venue once.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/dex/pkg/zapwire"
|
||||
"github.com/luxfi/rpc"
|
||||
)
|
||||
|
||||
// read-only observation methods served by cmd/dvenue (same as fourpath-live).
|
||||
const (
|
||||
clobDepthMethod = "clob_depth"
|
||||
clobBalanceMethod = "clob_balance"
|
||||
)
|
||||
|
||||
// txDepositTag is dchain.TxDeposit's wire tag, mixed into the content ref so deposits are
|
||||
// exactly-once on retry. Kept in sync with pkg/dchain tx kinds.
|
||||
const txDepositTag = 1
|
||||
|
||||
// venueClient is a thin live ZAP client over rpc.ZAPDial.
|
||||
type venueClient struct {
|
||||
conn *rpc.ZAPConn
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// dialVenue opens a ZAP connection to the venue at addr (host:port). The returned close
|
||||
// func releases the connection and the dial context.
|
||||
func dialVenue(ctx context.Context, addr string) (*venueClient, func(), error) {
|
||||
dctx, cancel := context.WithTimeout(ctx, 60*time.Second)
|
||||
conn, err := rpc.ZAPDial(dctx, addr)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, nil, fmt.Errorf("ZAPDial(%s): %w", addr, err)
|
||||
}
|
||||
return &venueClient{conn: conn, ctx: dctx}, func() { _ = conn.Close(); cancel() }, nil
|
||||
}
|
||||
|
||||
// poolIDForSymbol mirrors pkg/dchain.poolIDForSymbol: symbol bytes, high byte tagged 0xD0
|
||||
// so an all-symbol-prefix pool can't collide with a zero id.
|
||||
func poolIDForSymbol(sym string) [32]byte {
|
||||
var p [32]byte
|
||||
copy(p[:], sym)
|
||||
p[31] = 0xD0
|
||||
return p
|
||||
}
|
||||
|
||||
// assetID mirrors pkg/dchain.a32: a label packed big-endian into the low 8 bytes.
|
||||
func assetID(label uint64) [32]byte {
|
||||
var a [32]byte
|
||||
binary.BigEndian.PutUint64(a[24:32], label)
|
||||
return a
|
||||
}
|
||||
|
||||
// contentRef mirrors pkg/dchain.contentRef so deposits are exactly-once on retry and
|
||||
// genuinely distinct otherwise.
|
||||
func contentRef(typ byte, user string, asset [32]byte, amount uint64) [32]byte {
|
||||
var ref [32]byte
|
||||
copy(ref[:], asset[:])
|
||||
ref[0] ^= typ
|
||||
for i := 0; i < len(user) && i+1 < 24; i++ {
|
||||
ref[1+i] ^= user[i]
|
||||
}
|
||||
binary.BigEndian.PutUint64(ref[24:32], binary.BigEndian.Uint64(asset[24:32])^amount)
|
||||
return ref
|
||||
}
|
||||
|
||||
func (c *venueClient) ensureMarket(pool [32]byte) error {
|
||||
if _, err := c.conn.Call(c.ctx, zapwire.MethodEnsureMarket, zapwire.EncodeEnsureMarket(pool)); err != nil {
|
||||
return fmt.Errorf("clob_ensure_market: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *venueClient) openMarket(pool, base, quote [32]byte) error {
|
||||
resp, err := c.conn.Call(c.ctx, zapwire.MethodOpenMarket, zapwire.EncodeOpenMarket(pool, base, quote))
|
||||
if err != nil {
|
||||
return fmt.Errorf("clob_open_market: %w", err)
|
||||
}
|
||||
if len(resp) < 9 || resp[8] != zapwire.StatusPlaced {
|
||||
return fmt.Errorf("clob_open_market not placed: resp=%x", resp)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deposit credits user's available balance for asset and waits for the sealer to credit it.
|
||||
// The live venue ACKs StatusPlaced (accepted) but credits when the block seals, so it
|
||||
// confirms via clob_balance rather than a synchronous echo (mirrors fourpath-live.deposit).
|
||||
func (c *venueClient) deposit(user string, asset [32]byte, amount uint64) error {
|
||||
ref := contentRef(byte(txDepositTag), user, asset, amount)
|
||||
resp, err := c.conn.Call(c.ctx, zapwire.MethodDeposit, zapwire.EncodeDeposit(user, asset, amount, ref))
|
||||
if err != nil {
|
||||
return fmt.Errorf("clob_deposit %s %d: %w", user, amount, err)
|
||||
}
|
||||
if status, _, derr := zapwire.DecodeBalanceResp(resp); derr == nil && status == zapwire.StatusRejected {
|
||||
return fmt.Errorf("clob_deposit %s %d: REJECTED", user, amount)
|
||||
}
|
||||
return c.waitAvailable(user, asset, amount)
|
||||
}
|
||||
|
||||
// place rests a limit order (e.g. a maker ask) and confirms it was accepted.
|
||||
func (c *venueClient) place(pool [32]byte, side uint8, price, size float64, user string) error {
|
||||
resp, err := c.conn.Call(c.ctx, zapwire.MethodPlace, zapwire.EncodePlace(pool, side, price, size, user))
|
||||
if err != nil {
|
||||
return fmt.Errorf("clob_place %s: %w", user, err)
|
||||
}
|
||||
if _, status, derr := zapwire.DecodeAck(resp); derr != nil || status != zapwire.StatusPlaced {
|
||||
return fmt.Errorf("clob_place %s: status=%d err=%v", user, status, derr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// balance reads clob_balance: (available, locked).
|
||||
func (c *venueClient) balance(user string, asset [32]byte) (available, locked uint64, err error) {
|
||||
req := make([]byte, zapwire.UserSize+zapwire.AssetIDSize)
|
||||
copy(req[0:zapwire.UserSize], user)
|
||||
copy(req[zapwire.UserSize:], asset[:])
|
||||
resp, err := c.conn.Call(c.ctx, clobBalanceMethod, req)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("clob_balance %s: %w", user, err)
|
||||
}
|
||||
if len(resp) < 16 {
|
||||
return 0, 0, fmt.Errorf("clob_balance short resp: %x", resp)
|
||||
}
|
||||
return binary.BigEndian.Uint64(resp[0:8]), binary.BigEndian.Uint64(resp[8:16]), nil
|
||||
}
|
||||
|
||||
// depth reads clob_depth: (orders, remaining, ok).
|
||||
func (c *venueClient) depth(pool [32]byte) (orders int, remaining float64, ok bool, err error) {
|
||||
resp, err := c.conn.Call(c.ctx, clobDepthMethod, pool[:])
|
||||
if err != nil {
|
||||
return 0, 0, false, fmt.Errorf("clob_depth: %w", err)
|
||||
}
|
||||
if len(resp) < 29 {
|
||||
return 0, 0, false, fmt.Errorf("clob_depth short resp: %x", resp)
|
||||
}
|
||||
return int(binary.BigEndian.Uint32(resp[0:4])), math.Float64frombits(binary.BigEndian.Uint64(resp[4:12])), resp[28] == 1, nil
|
||||
}
|
||||
|
||||
// waitAvailable polls clob_balance until available >= want (the credit has sealed) or
|
||||
// times out.
|
||||
func (c *venueClient) waitAvailable(user string, asset [32]byte, want uint64) error {
|
||||
deadline := time.Now().Add(20 * time.Second)
|
||||
for {
|
||||
a, _, err := c.balance(user, asset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if a >= want {
|
||||
return nil
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
a, lck, _ := c.balance(user, asset)
|
||||
return fmt.Errorf("clob_deposit/credit %s: available=%d locked=%d, want >=%d (sealer not crediting)", user, a, lck, want)
|
||||
}
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// waitDepth polls until the book's order count and remaining match (the rested ask sealed).
|
||||
func (c *venueClient) waitDepth(pool [32]byte, wantOrders int, wantRem float64) error {
|
||||
deadline := time.Now().Add(20 * time.Second)
|
||||
for {
|
||||
n, rem, ok, err := c.depth(pool)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok && n == wantOrders && rem == wantRem {
|
||||
return nil
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
return fmt.Errorf("book orders=%d remaining=%v ok=%v, want %d / %v", n, rem, ok, wantOrders, wantRem)
|
||||
}
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// seedSpec describes one maker book to seed: a market (base/quote) with a single resting
|
||||
// maker ask of askSize at askPrice. The keeper seeds this so a Phase-A taker BUY routed
|
||||
// through the seam fills exactly one ask.
|
||||
type seedSpec struct {
|
||||
Symbol string
|
||||
Base [32]byte
|
||||
Quote [32]byte
|
||||
Maker string
|
||||
AskSize float64
|
||||
AskPrice float64
|
||||
}
|
||||
|
||||
// seedMakerLiquidity ensures+opens the market, deposits the maker's base inventory, rests
|
||||
// one maker ask, and waits for both the deposit credit and the resting ask to seal. It is
|
||||
// idempotent on retry via content-addressed deposit refs (a re-run with the same spec does
|
||||
// not double-credit). Returns the pool id of the seeded market.
|
||||
func (c *venueClient) seedMakerLiquidity(s seedSpec) ([32]byte, error) {
|
||||
pool := poolIDForSymbol(s.Symbol)
|
||||
if err := c.ensureMarket(pool); err != nil {
|
||||
return pool, err
|
||||
}
|
||||
if err := c.openMarket(pool, s.Base, s.Quote); err != nil {
|
||||
return pool, err
|
||||
}
|
||||
// Maker funds AskSize of base inventory so the resting ask is fully collateralised.
|
||||
makerBase := uint64(math.Ceil(s.AskSize))
|
||||
if err := c.deposit(s.Maker, s.Base, makerBase); err != nil {
|
||||
return pool, err
|
||||
}
|
||||
if err := c.place(pool, zapwire.SideSell, s.AskPrice, s.AskSize, s.Maker); err != nil {
|
||||
return pool, err
|
||||
}
|
||||
if err := c.waitDepth(pool, 1, s.AskSize); err != nil {
|
||||
return pool, err
|
||||
}
|
||||
return pool, nil
|
||||
}
|
||||
@@ -5,21 +5,16 @@ go 1.26.4
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
|
||||
github.com/luxfi/chains v1.3.16
|
||||
github.com/luxfi/consensus v1.25.18
|
||||
github.com/luxfi/crypto v1.19.21
|
||||
github.com/luxfi/database v1.20.3
|
||||
github.com/luxfi/genesis v1.13.14
|
||||
github.com/luxfi/geth v1.17.12
|
||||
github.com/luxfi/go-bip32 v1.0.2
|
||||
github.com/luxfi/go-bip39 v1.1.2
|
||||
github.com/luxfi/ids v1.2.15
|
||||
github.com/luxfi/log v1.4.3
|
||||
github.com/luxfi/pq v1.0.3
|
||||
github.com/luxfi/rpc v1.1.0
|
||||
github.com/luxfi/version v1.0.1
|
||||
github.com/luxfi/vm v1.2.5
|
||||
github.com/luxfi/zap v0.8.10
|
||||
github.com/nats-io/nats.go v1.52.0
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
@@ -52,7 +47,6 @@ require (
|
||||
github.com/aws/smithy-go v1.24.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/btcsuite/btcd/btcutil v1.1.6 // indirect
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
@@ -89,7 +83,6 @@ require (
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/luxfi/accel v1.2.4 // indirect
|
||||
github.com/luxfi/address v1.0.1 // indirect
|
||||
github.com/luxfi/age v1.5.0 // indirect
|
||||
github.com/luxfi/api v1.0.15 // indirect
|
||||
github.com/luxfi/atomic v1.0.0 // indirect
|
||||
@@ -106,15 +99,16 @@ require (
|
||||
github.com/luxfi/metric v1.5.9 // indirect
|
||||
github.com/luxfi/mock v0.1.1 // indirect
|
||||
github.com/luxfi/p2p v1.21.1 // indirect
|
||||
github.com/luxfi/precompile v0.5.56 // indirect
|
||||
github.com/luxfi/proto v1.3.5 // indirect
|
||||
github.com/luxfi/resource v0.0.1 // indirect
|
||||
github.com/luxfi/runtime v1.1.1 // indirect
|
||||
github.com/luxfi/sampler v1.1.0 // indirect
|
||||
github.com/luxfi/tls v1.0.3 // indirect
|
||||
github.com/luxfi/upgrade v1.0.1 // indirect
|
||||
github.com/luxfi/utils v1.2.0 // indirect
|
||||
github.com/luxfi/validators v1.2.0 // indirect
|
||||
github.com/luxfi/warp v1.19.5 // indirect
|
||||
github.com/luxfi/zap v0.8.10 // indirect
|
||||
github.com/luxfi/zapdb v1.10.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
|
||||
@@ -8,7 +8,6 @@ github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
|
||||
github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
|
||||
@@ -51,29 +50,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
|
||||
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
||||
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
|
||||
github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
||||
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
||||
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
|
||||
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
|
||||
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
|
||||
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
@@ -104,7 +82,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
|
||||
github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc=
|
||||
github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
@@ -113,13 +90,10 @@ github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
|
||||
github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
|
||||
github.com/deckarep/golang-set/v2 v2.9.0 h1:prva4eP9UysWagLyKrtn074ughi0NnkIf0A4M5yOCKI=
|
||||
github.com/deckarep/golang-set/v2 v2.9.0/go.mod h1:EWknQXbs0mcFpat2QOoXV0Ee57cD+w6ZEN76BR2JVrM=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||
github.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU=
|
||||
github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
|
||||
@@ -187,7 +161,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/rpc v1.2.1 h1:yC+LMV5esttgpVvNORL/xX4jvTTEUE30UZhZ5JF7K9k=
|
||||
github.com/gorilla/rpc v1.2.1/go.mod h1:uNpOihAlF5xRFLuTYhfR0yfCTm0WTQSQttkMSptRfGk=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
|
||||
@@ -210,12 +183,8 @@ github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFck
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
@@ -243,8 +212,6 @@ github.com/luxfi/atomic v1.0.0 h1:xUV60MuzRvXngaQ1sM0yVC2v4TRoLlUGkkH7M9PS4yw=
|
||||
github.com/luxfi/atomic v1.0.0/go.mod h1:0G2mTlQ6TXWHICUHrUUPu1/qAiIyR4gSZ2tva9ci/bI=
|
||||
github.com/luxfi/cache v1.2.1 h1:kAzOS55/hmYeNKR+0HAKv4ma48Y6JjkI8UQeqdZ8bfI=
|
||||
github.com/luxfi/cache v1.2.1/go.mod h1:co7JTxZZHpKT31Yh01LFp5aZOxmoUg157FhBLQdQHVU=
|
||||
github.com/luxfi/chains v1.3.16 h1:oCInwe650dBx53NdA0fg/gGmkNXOXg5NWpUSvhN6Iyc=
|
||||
github.com/luxfi/chains v1.3.16/go.mod h1:lFwRZno8sDOfLuWyCi1rMxum9DPFn6aqupEEpIat9Mk=
|
||||
github.com/luxfi/compress v0.0.5 h1:4tEUHw5MK1bu5UOjfYCt4OKMiH7yykIgmGPRA/BfJTM=
|
||||
github.com/luxfi/compress v0.0.5/go.mod h1:Cc1yxD2pfzrvpO32W2GDwLKff+CylHEvzZh2Ko8RSIU=
|
||||
github.com/luxfi/concurrent v0.0.3 h1:eJyv1fhaC0jMLMw6+QS774cUmp7GK+ouMgvLCqnC7cc=
|
||||
@@ -267,14 +234,8 @@ github.com/luxfi/filesystem v0.0.1 h1:VZ6xMFKaAPBW/ddlMsDnI2G0VU1lV5rYaVcW5d+KwE
|
||||
github.com/luxfi/filesystem v0.0.1/go.mod h1:OQVSU6XNwqrr1AI+MqkID2taHUclx7NYmmr3svgttec=
|
||||
github.com/luxfi/formatting v1.0.1 h1:ZnE1rAdEUds9yAegdVdGDOBGN6hLMPOv6E03Fp8IEYo=
|
||||
github.com/luxfi/formatting v1.0.1/go.mod h1:mYzNf5DJOiqSSKUPzNj5dKy4tstFbN3pZlkI5716eKc=
|
||||
github.com/luxfi/genesis v1.13.14 h1:CauxugSR1NP6Zhng+6BvBvXjTnbMwQrfGILg5a9LbR0=
|
||||
github.com/luxfi/genesis v1.13.14/go.mod h1:qUa+AcTWwxv0x+CJochBsRNOMbmEBjw07HJKZLhs5c0=
|
||||
github.com/luxfi/geth v1.17.12 h1:UP/fhpcfbGPTrkOCwX3d88Oc3jVm5gTOgfjgq+lek6s=
|
||||
github.com/luxfi/geth v1.17.12/go.mod h1:3vQfQJd9JC+AVBjxNXa9PYQOqpbE2dKu8E3jqhPZ3LU=
|
||||
github.com/luxfi/go-bip32 v1.0.2 h1:7vFbb+Wr4Z499q2tuCLdd7wWjtn8sH+HWBlx76mhH9Y=
|
||||
github.com/luxfi/go-bip32 v1.0.2/go.mod h1:bc7/LXDKAJQZ/F0Xjf5yXaTZxY9/ssLb4FC+Hxn/cDk=
|
||||
github.com/luxfi/go-bip39 v1.1.2 h1:p+wLMPGs6MLQh7q0YIsmy2EhHL7LHiELEGTJko6t/Jg=
|
||||
github.com/luxfi/go-bip39 v1.1.2/go.mod h1:96de9VkR2kY/ASAnhMtvt3TSh+PZkAFAngNj0GjRGDo=
|
||||
github.com/luxfi/ids v1.2.15 h1:omE+E4+0Poj9DzM11ejSFgteaSQ3KDHi5g54iH6jcxI=
|
||||
github.com/luxfi/ids v1.2.15/go.mod h1:Fj73K5xcblvdE0SxU/ip+jE8VqNdu+80548su5KJ7xI=
|
||||
github.com/luxfi/keychain v1.0.2 h1:uQgmjs37/VBIALEiYrrszTpxvtqr07/YvS9TnmxGafs=
|
||||
@@ -311,8 +272,6 @@ github.com/luxfi/sampler v1.1.0 h1:u3iRDl7V06ARh0e85h3HT+aZ1saCFo2yMMsh+dCJbqk=
|
||||
github.com/luxfi/sampler v1.1.0/go.mod h1:kJa53S3tC9+VSbuV3RFu68MmbCCBlr2UM39LOClQ/Hs=
|
||||
github.com/luxfi/timer v1.0.2 h1:g/odi0VQJIsrzdklJUG1thHZ/sGNnbIiVGcU6LctJm0=
|
||||
github.com/luxfi/timer v1.0.2/go.mod h1:SoaZwntYigUE3H6z1GV32YwP8QaSiAT0UiEv7iPugXg=
|
||||
github.com/luxfi/tls v1.0.3 h1:rK3nxSAxrUOOSHOZnKChwV4f6UJ+cfOl8KWJXAQx/SI=
|
||||
github.com/luxfi/tls v1.0.3/go.mod h1:dQqSiGE7YxXUxOwICoReUuIitBms9DYOaCeteBwmIWw=
|
||||
github.com/luxfi/upgrade v1.0.1 h1:7+ygYeUf/MuLeGL7pjIu6ckQimxctCp+Swybhpy64go=
|
||||
github.com/luxfi/upgrade v1.0.1/go.mod h1:Re7g9Y+SYf/LvkHFpN0vbtlVH/Rr5ZpHQdPeVFEo3Jw=
|
||||
github.com/luxfi/utils v1.2.0 h1:gtEiI7/NM6PQ/OasEpH0PvB+e5hIS/tpum9r64pYjMc=
|
||||
@@ -366,15 +325,11 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
||||
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
@@ -425,19 +380,12 @@ github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMT
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE=
|
||||
github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
|
||||
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
|
||||
@@ -482,7 +430,6 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
@@ -495,7 +442,6 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@@ -503,7 +449,6 @@ golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
@@ -526,8 +471,6 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -542,7 +485,6 @@ golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
@@ -587,12 +529,10 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
# dexkeeper — devnet 0x9999 DEX-seam deploy
|
||||
|
||||
The keeper that makes the on-chain **0x9999 two-phase atomic DEX swap flow**
|
||||
operational on **devnet** so the first real `DEXFill` lands and the V4/DEX markets
|
||||
populate. DEVNET ONLY.
|
||||
|
||||
## The seam (what the keeper drives)
|
||||
|
||||
```
|
||||
C Phase-A intent (signed swap to 0x9999, DI01 hookData)
|
||||
-> SubmitSwapIntent locks input + stages C->D object (key=intentID) + emits IntentSubmitted
|
||||
KEEPER watches IntentSubmitted
|
||||
-> dexvm ImportTx (consume C->D object, fund D order) via dex.submitTx
|
||||
-> dexvm settling RelayOrderTx (clob_submit, CollateralRef=intentID) via dex.submitTx
|
||||
dexvm proposer relays ONCE (obtainFills) -> fills carried -> settleFromFills exports D->C proceeds
|
||||
KEEPER polls dex.getSettlement(intentID) -> {outputID, amount}
|
||||
-> C Phase-B settle (signed swap to 0x9999, DS01 outputID|amount|intentID)
|
||||
-> ImportSettlement credits maker + emits DEXFill <-- the goal
|
||||
```
|
||||
|
||||
## Prerequisites (status)
|
||||
|
||||
1. **dexvm relay wired** — DONE. All 5 `lux-devnet` validators log
|
||||
`relayConfigured:true -> dchain-venue:9099` (per-chain config.json seeded by the
|
||||
`luxd-startup` CM; mirrored to source in `universe` `feat/dexvm-relay-devnet`).
|
||||
2. **dexvm RPC seam** — code in `chains` `feat/dexvm-submit-tx-rpc`: `dex.submitTx`
|
||||
(mempool entry) + `dex.getSettlement` (proceeds-coordinate query). The luxd node
|
||||
image must be rebuilt with the new `dexvm-linux-amd64` plugin (S3) and the
|
||||
validators rolled onto it.
|
||||
3. **keeper image** — `ghcr.io/luxfi/dexkeeper` (this repo, `docker.yml` matrix +
|
||||
`Dockerfile.dexkeeper`). Requires `zap >= v0.8.10` and a `chains` tag carrying the
|
||||
RPC seam to be cut first (the keeper imports both).
|
||||
4. **maker key in KMS** — the devnet mnemonic is already in lux KMS; `kmssecret-mnemonic.yaml`
|
||||
syncs it to the `dexkeeper-mnemonic` secret. The keeper derives the maker
|
||||
(`0x9011…`, `m/44'/9000'/0'/0/i`) in-process.
|
||||
|
||||
## Deploy sequence (CI/CD + operator only)
|
||||
|
||||
```sh
|
||||
# 0) Cut tags so the keeper image builds from published modules:
|
||||
# - luxfi/zap v0.8.10 (EncodeIntentHookData / EncodeSettlementHookData)
|
||||
# - luxfi/chains <tag> (dexvm dex.submitTx + dex.getSettlement)
|
||||
# 1) Rebuild the node image + dexvm-linux-amd64 plugin (chains tag), publish to S3,
|
||||
# bump the lux-devnet LuxNetwork CR pluginSource, roll the 5 luxd pods (OnDelete).
|
||||
# 2) Build the keeper image (push to main / tag v* triggers docker.yml).
|
||||
# 3) Sync the maker key + deploy the keeper:
|
||||
kubectl --context do-sfo3-lux-k8s apply -f kmssecret-mnemonic.yaml
|
||||
kubectl --context do-sfo3-lux-k8s apply -f deployment.yaml
|
||||
# 4) Seed venue liquidity + drive the first swap:
|
||||
kubectl --context do-sfo3-lux-k8s apply -f job-drive-first-swap.yaml
|
||||
```
|
||||
|
||||
## Verify
|
||||
|
||||
```sh
|
||||
# first DEXFill on devnet C (report the tx hash):
|
||||
curl -s -X POST https://api.lux-dev.network/ext/bc/C/rpc -H 'content-type:application/json' \
|
||||
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getLogs","params":[{"address":"0x0000000000000000000000000000000000009999","fromBlock":"0x0","toBlock":"latest"}]}'
|
||||
# DEXFill topic0 = keccak256("DEXFill(bytes32,address,uint256,uint256)")
|
||||
|
||||
# dex subgraph shows the market + fill (not markets:null):
|
||||
curl -s -X POST https://explorer.lux-dev.network/v1/graph/cchain/dex/graphql \
|
||||
-H 'content-type:application/json' -d '{"query":"{ markets { id baseAsset quoteAsset } fills(first:3){ id } }"}'
|
||||
|
||||
# UI: lux.exchange?network=devnet renders the V4/DEX market.
|
||||
```
|
||||
@@ -1,68 +0,0 @@
|
||||
# dexkeeper Deployment (lux-devnet): the off-chain keeper that drives the on-chain
|
||||
# 0x9999 two-phase atomic DEX seam so DEXFill lands on the devnet C-Chain.
|
||||
#
|
||||
# It watches the C-Chain 0x9999 IntentSubmitted event, submits the dexvm ImportTx +
|
||||
# settling RelayOrderTx (via dex.submitTx), polls dex.getSettlement for the D->C
|
||||
# proceeds object, then has the maker sign + send the C-side Phase-B settle (which
|
||||
# emits DEXFill). The maker key is derived in-process from LUX_MNEMONIC (KMS-synced;
|
||||
# see kmssecret-mnemonic.yaml).
|
||||
#
|
||||
# PRECONDITIONS (all in this work's branches):
|
||||
# - luxd validators on a node image whose dexvm includes dex.submitTx + dex.getSettlement
|
||||
# (chains feat/dexvm-submit-tx-rpc) and relayConfigured:true (done — luxd-startup CM).
|
||||
# - dchain-venue:9099 reachable (done).
|
||||
# - dexkeeper image built (ghcr.io/luxfi/dexkeeper, docker.yml matrix).
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: dexkeeper
|
||||
namespace: lux-devnet
|
||||
labels:
|
||||
app.kubernetes.io/name: dexkeeper
|
||||
app.kubernetes.io/part-of: lux-dex
|
||||
spec:
|
||||
replicas: 1 # single keeper: relays are proposer-relays-once; a 2nd keeper would
|
||||
# double-submit ImportTx/RelayOrderTx (idempotent on the dexvm side by
|
||||
# content-addressed UTXO/receipt, but no need). Scale only with leader-election.
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: dexkeeper
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: dexkeeper
|
||||
app.kubernetes.io/part-of: lux-dex
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: ghcr-luxfi
|
||||
containers:
|
||||
- name: dexkeeper
|
||||
image: ghcr.io/luxfi/dexkeeper:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- --mode=watch
|
||||
- --c-rpc=http://luxd-0.luxd-headless.lux-devnet.svc.cluster.local:9650/ext/bc/C/rpc
|
||||
- --d-rpc=http://luxd-0.luxd-headless.lux-devnet.svc.cluster.local:9650/ext/bc/28mZwmZtKAdGWoJRWSYw8RXxUcHZUDVsNgjMH4nnMfoJGJi2SX/rpc
|
||||
- --d-chain-id=28mZwmZtKAdGWoJRWSYw8RXxUcHZUDVsNgjMH4nnMfoJGJi2SX
|
||||
- --venue=dchain-venue.lux-devnet.svc.cluster.local:9099
|
||||
- --maker=0x9011E888251AB053B7bD1cdB598Db4f9DEd94714
|
||||
env:
|
||||
# The BIP-39 mnemonic the keeper derives the maker key from (KMS-synced).
|
||||
- name: LUX_MNEMONIC
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: dexkeeper-mnemonic
|
||||
key: LUX_MNEMONIC
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
@@ -1,62 +0,0 @@
|
||||
# One-shot Job: seed venue liquidity + drive the FIRST Phase-A 0x9999 swap from the
|
||||
# maker, in-cluster (the maker key is derived from the KMS-synced LUX_MNEMONIC; nothing
|
||||
# is handled out-of-band). The running dexkeeper Deployment then drives Phase B -> the
|
||||
# first DEXFill. Use this to seed + kick the first trade; delete after it Completes.
|
||||
#
|
||||
# Modes (--mode):
|
||||
# seed — only seed the venue (clob_deposit + maker resting ask) over ZAP.
|
||||
# phase-a — seed + send ONE Phase-A intent swap from the maker; prints intentID + txHash.
|
||||
#
|
||||
# After this Completes, verify:
|
||||
# eth_getLogs(address=0x9999, IntentSubmitted) on devnet C returns the intent;
|
||||
# the dexkeeper logs show ImportTx + RelayOrderTx submitted + Phase-B settle;
|
||||
# eth_getLogs(address=0x9999, DEXFill) returns the first fill.
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: dexkeeper-drive-first-swap
|
||||
namespace: lux-devnet
|
||||
labels:
|
||||
app.kubernetes.io/name: dexkeeper
|
||||
app.kubernetes.io/part-of: lux-dex
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 600
|
||||
backoffLimit: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: dexkeeper
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
imagePullSecrets:
|
||||
- name: ghcr-luxfi
|
||||
containers:
|
||||
- name: dexkeeper
|
||||
image: ghcr.io/luxfi/dexkeeper:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- --mode=phase-a
|
||||
- --c-rpc=http://luxd-0.luxd-headless.lux-devnet.svc.cluster.local:9650/ext/bc/C/rpc
|
||||
- --d-rpc=http://luxd-0.luxd-headless.lux-devnet.svc.cluster.local:9650/ext/bc/28mZwmZtKAdGWoJRWSYw8RXxUcHZUDVsNgjMH4nnMfoJGJi2SX/rpc
|
||||
- --d-chain-id=28mZwmZtKAdGWoJRWSYw8RXxUcHZUDVsNgjMH4nnMfoJGJi2SX
|
||||
- --venue=dchain-venue.lux-devnet.svc.cluster.local:9099
|
||||
- --maker=0x9011E888251AB053B7bD1cdB598Db4f9DEd94714
|
||||
env:
|
||||
- name: LUX_MNEMONIC
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: dexkeeper-mnemonic
|
||||
key: LUX_MNEMONIC
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
Reference in New Issue
Block a user