chains/dexvm: atomic custody rail (custody.go) — deposit=import+credit, withdraw=debit+export

executeDeposit = executeImport (atomic C/X debit, consume-once) + clob_deposit relay (credit
D-Chain balance; refuses credited!=imported). executeWithdraw = clob_withdraw (debit, realized
clamped) + executeExport (credit exactly realized; refuses realized>want). Reuses the
conservation-tested ImportTx/ExportTx rail. custody_conservation_test: real atomic.SharedMemory,
deposit→trade→withdraw conserves C in==out, UTXO consumed once. NOTE: not yet wired into the
carried-fills BuildBlock plan (network-upgrade-gated follow-up, like #9).
This commit is contained in:
2026-06-17 21:10:33 -07:00
parent 9f96a5cae4
commit 8f733194b3
3 changed files with 577 additions and 0 deletions
+199
View File
@@ -0,0 +1,199 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package dexvm
import (
"context"
"encoding/binary"
"fmt"
"github.com/luxfi/ids"
"github.com/luxfi/chains/dexvm/txs"
)
// custody.go is the FUNDS-IN / FUNDS-OUT rail that connects the proxy's atomic
// shared-memory leg (atomic.go) to the D-Chain balance ledger (where the money
// lives in the order book). It is the CLOB deposit/withdraw model, decomplected
// into the two primitives the proxy already owns plus a single relay:
//
// DEPOSIT = atomic IMPORT (debit C/X, consume the UTXO exactly once) THEN
// relay clob_deposit (credit the account's available D-Chain balance).
// The credited D-Chain amount EQUALS the imported value — no escrow,
// no settlement math; the value simply MOVES into the book's ledger.
//
// WITHDRAW = relay clob_withdraw (debit the account's realized D-Chain balance,
// returning the REALIZED amount the ledger actually released) THEN
// atomic EXPORT of exactly that realized amount (credit C/X).
// A withdraw can only export realized, un-escrowed balance, so the
// exported value never exceeds what the ledger debited — no mint.
//
// CONSERVATION (the whole point): every unit that leaves C/X on a deposit import
// is credited into the D-Chain ledger; every unit the ledger releases on a
// withdraw is exported back to C/X. Across deposit -> (trade inside the D-Chain)
// -> withdraw, C/X value out == C/X value in for each account, to the unit. The
// trade itself conserves inside the D-Chain ledger (dchain.settleFills moves
// maker<->taker exact integers); the proxy never mints because it only ever
// imports (consume-once) and exports (realized-only).
//
// CONSENSUS NOTE (the relay LOCATION): exactly like the order relay (RED #9),
// the irreversible/non-deterministic D-Chain relay for deposit/withdraw must run
// ONCE network-wide at the PROPOSER's BuildBlock and have its result CARRIED in
// the block bytes, so every validator settles the atomic leg from carried bytes
// (never a per-validator relay). The primitives here are the relay+atomic
// composition; wiring them into BuildBlock's carried-result plan (a withdraw
// carries its realized amount; a deposit carries only an ack) is the same
// proposer-once+carry discipline obtainFills/settleCarried already implement for
// submits, and the block/vertex wire that carries them is the network-upgrade-
// gated change. These functions are written to be driven from that plan: deposit
// returns nothing to carry (idempotent credit), withdraw returns the realized
// amount to carry into the export leg.
// assetHandle folds a 32-byte cross-chain asset id to the 8-byte asset handle the
// D-Chain ledger keys balances by (big-endian over the leading 8 bytes). The same
// fold MUST be used wherever a balance is credited/debited for an asset, so the
// deposit and withdraw of one asset agree on its handle. (The D-Chain side keys
// balance:<user:8><asset:8>; this is the proxy's view of that 8-byte handle.)
func assetHandle(asset ids.ID) uint64 {
return binary.BigEndian.Uint64(asset[:8])
}
// userHandle renders a 20-byte account address to the 16-byte user identity the
// frozen CLOB frames carry (the leading 16 bytes of the address, the same width
// zapwire.UserSize uses). The D-Chain folds this 16-byte user to its 8-byte
// UserID; binding the proxy's deposit/withdraw to the same 16-byte slice keeps
// the credited/debited account consistent with the orders that account places.
func userHandle(addr ids.ShortID) string {
return string(addr[:])
}
// encodeDepositFrame builds the FROZEN clob_deposit payload: user[16] + asset[8]
// + amount[8]. Byte-identical with github.com/luxfi/dex/pkg/zapwire.EncodeDeposit
// (the proxy cannot import the cgo-tagged d-chain package; a parity test pins it).
func encodeDepositFrame(user string, asset, amount uint64) []byte {
out := make([]byte, DepositReqSize)
copy(out[0:16], padUser(user))
binary.BigEndian.PutUint64(out[16:24], asset)
binary.BigEndian.PutUint64(out[24:32], amount)
return out
}
// encodeWithdrawFrame builds the FROZEN clob_withdraw payload: user[16] +
// asset[8] + amount[8].
func encodeWithdrawFrame(user string, asset, amount uint64) []byte {
out := make([]byte, WithdrawReqSize)
copy(out[0:16], padUser(user))
binary.BigEndian.PutUint64(out[16:24], asset)
binary.BigEndian.PutUint64(out[24:32], amount)
return out
}
// padUser left-copies a user identity into a fresh 16-byte (UserSize) buffer,
// null-padded — the frozen frame's user field encoding.
func padUser(user string) []byte {
b := make([]byte, 16)
copy(b, user)
return b
}
// decodeBalanceResp reads (status, realized amount) from a clob_deposit /
// clob_withdraw response (status[1] + amount[8]). A short response is malformed
// wire and is refused — the proxy never derives a settlement amount from garbage.
func decodeBalanceResp(resp []byte) (status uint8, amount uint64, err error) {
if len(resp) < BalanceRespSize {
return 0, 0, fmt.Errorf("dexvm custody: balance response too short: %d", len(resp))
}
return resp[0], binary.BigEndian.Uint64(resp[1:9]), nil
}
// executeDeposit runs the DEPOSIT rail for one import: atomically claim the
// imported value (consume-once) and relay a clob_deposit to credit exactly that
// value into the importer's available D-Chain balance. The credited amount is the
// import's total output value (the value locked by the atomic leg), so the
// D-Chain ledger gains precisely what C/X lost — conservation across the rail.
//
// The relay is the irreversible/non-deterministic leg; in production it runs once
// at the proposer's BuildBlock (carried as an idempotent ack). Here it composes
// the already-tested executeImport with the relay so a caller (BuildBlock plan,
// or the conservation test) gets the full funds-in path in one call.
func (vm *VM) executeDeposit(ctx context.Context, tx *txs.ImportTx, ar *atomicRequests) error {
// 1) Atomic import: consume the source UTXO(s), accumulate the remove. This is
// the SAME consume-once-guarded primitive the escrow path used; the only
// difference is what happens to the value next (credited to the D-Chain
// ledger, not recorded as proxy escrow).
if err := vm.executeImport(tx, ar); err != nil {
return err
}
// 2) Credit the D-Chain ledger with exactly the imported value, in the imported
// asset, for the importer. A deposit with no credited outputs (pure fee
// burn) credits nothing.
if len(tx.Outputs) == 0 {
return nil
}
owner := tx.Outputs[0].Owner
asset := assetHandle(tx.Outputs[0].Asset)
var amount uint64
for _, o := range tx.Outputs {
// All outputs of a single deposit are the same (owner, asset) — sum the
// imported value into one ledger credit.
amount += o.Amount
}
frame := encodeDepositFrame(userHandle(owner), asset, amount)
resp, err := vm.relay.Relay(ctx, ZAPMethodDeposit, frame)
if err != nil {
return fmt.Errorf("dexvm custody: deposit relay: %w", err)
}
status, credited, derr := decodeBalanceResp(resp)
if derr != nil {
return derr
}
// The D-Chain must credit exactly the imported value; a short credit would
// strand value (imported but not in the ledger). Refuse the divergence rather
// than silently lose funds.
if credited != amount {
return fmt.Errorf("dexvm custody: deposit credited %d != imported %d (status %d)", credited, amount, status)
}
return nil
}
// executeWithdraw runs the WITHDRAW rail for one account/asset: relay a
// clob_withdraw to debit the account's realized available D-Chain balance
// (returning the REALIZED amount the ledger released, clamped to availability),
// then atomically EXPORT exactly that realized amount back to the destination
// chain. The export value is the ledger's realized debit, so C/X gains precisely
// what the D-Chain ledger released — never more (no mint).
//
// owner is the account (its address); asset is the cross-chain asset id; want is
// the requested withdrawal (the ledger clamps it to available). destChain is
// where the value is exported (C-Chain). fillRef binds the export for audit. The
// returned realized amount is what was exported (0 => nothing available, no export
// built). The relay is the irreversible leg (proposer-once + carry the realized
// amount in production).
func (vm *VM) executeWithdraw(ctx context.Context, owner ids.ShortID, asset ids.ID, want uint64, destChain, fillRef ids.ID, txIndex uint32, createdAt int64, ar *atomicRequests) (uint64, error) {
handle := assetHandle(asset)
frame := encodeWithdrawFrame(userHandle(owner), handle, want)
resp, err := vm.relay.Relay(ctx, ZAPMethodWithdraw, frame)
if err != nil {
return 0, fmt.Errorf("dexvm custody: withdraw relay: %w", err)
}
_, realized, derr := decodeBalanceResp(resp)
if derr != nil {
return 0, derr
}
if realized == 0 {
return 0, nil // nothing available to withdraw; no export leg
}
if realized > want {
// The ledger released MORE than requested — impossible for a clamped
// withdraw and a mint risk. Refuse rather than export the excess.
return 0, fmt.Errorf("dexvm custody: withdraw realized %d exceeds requested %d (mint risk)", realized, want)
}
// Atomic export of EXACTLY the realized amount in the withdrawn asset.
out := []txs.AtomicOutput{{Owner: owner, Asset: asset, Amount: realized}}
exportTx := txs.NewSettlementExportTx(owner, txIndex, destChain, out, fillRef, createdAt)
if err := vm.executeExport(exportTx, ar); err != nil {
return 0, fmt.Errorf("dexvm custody: withdraw export: %w", err)
}
return realized, nil
}
+354
View File
@@ -0,0 +1,354 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// custody_conservation_test.go proves the FULL CLOB custody rail conserves value
// across the C-Chain <-> D-Chain boundary for a NON-NATIVE asset: atomically
// DEPOSIT into the D-Chain (ImportTx + clob_deposit), TRADE inside the D-Chain
// (modeled by the ledger matcher moving balances), and WITHDRAW back out
// (clob_withdraw + ExportTx). The acceptance criterion is the same one the
// taker-only escrow model could not meet for the maker leg: C-Chain value OUT ==
// C-Chain value IN, to the unit, with the proxy minting NOTHING (import consumes
// once; export releases only realized ledger balance).
//
// REAL atomic.SharedMemory drives the import/export legs; a ledger-modeling fake
// matcher stands in for the D-Chain over the zapDialer seam, so clob_deposit
// credits and clob_withdraw debits a real in-test balance ledger — exactly what
// the dchain.VM does in consensus (pkg/dchain/conservation_test.go proves THAT
// side end-to-end against the real VM).
package dexvm
import (
"context"
"encoding/binary"
"testing"
"github.com/luxfi/database/memdb"
"github.com/luxfi/database/prefixdb"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/runtime"
"github.com/luxfi/vm"
"github.com/luxfi/vm/chains/atomic"
"github.com/luxfi/warp"
"github.com/luxfi/chains/dexvm/config"
"github.com/luxfi/chains/dexvm/txs"
)
// ledgerMatcher is a fake D-Chain over the zapConn seam that maintains a real
// per-(user,asset) available-balance ledger. clob_deposit credits it; clob_submit
// (a trade) moves balance between two accounts; clob_withdraw debits it (clamped
// to available) and reports the realized amount. It models EXACTLY the invariant
// the real d-chain enforces — sum of balances is conserved across deposit ->
// trade -> withdraw — so the proxy rail can be proven against a faithful stand-in.
type ledgerMatcher struct {
bal map[string]uint64 // key = user16 || asset8
received [][]byte
}
func newLedgerMatcher() *ledgerMatcher { return &ledgerMatcher{bal: map[string]uint64{}} }
func ledgerKey(user []byte, asset uint64) string {
var a [8]byte
binary.BigEndian.PutUint64(a[:], asset)
return string(user) + string(a[:])
}
func (m *ledgerMatcher) Call(_ context.Context, method string, payload []byte) ([]byte, error) {
m.received = append(m.received, append([]byte(nil), payload...))
switch method {
case ZAPMethodDeposit:
user := payload[0:16]
asset := binary.BigEndian.Uint64(payload[16:24])
amount := binary.BigEndian.Uint64(payload[24:32])
m.bal[ledgerKey(user, asset)] += amount
return balResp(0, amount), nil // credited exactly
case ZAPMethodWithdraw:
user := payload[0:16]
asset := binary.BigEndian.Uint64(payload[16:24])
want := binary.BigEndian.Uint64(payload[24:32])
k := ledgerKey(user, asset)
realized := want
if realized > m.bal[k] {
realized = m.bal[k] // clamp to available (no mint)
}
m.bal[k] -= realized
st := uint8(0)
if realized == 0 {
st = 2
}
return balResp(st, realized), nil
default:
// place/cancel/ensure/open acks.
ack := make([]byte, 9)
return ack, nil
}
}
func (m *ledgerMatcher) Close() error { return nil }
// trade moves `base` of `baseAsset` from seller->buyer and `quote` of `quoteAsset`
// from buyer->seller inside the ledger — what an accepted D-Chain block's
// settleFills does. Used by the test to model the cross between the deposit and
// withdraw legs (the proxy does not trade; the D-Chain does). buyer/seller are
// the 16-byte frame-form user identities (frameUser).
func (m *ledgerMatcher) trade(buyer, seller []byte, baseAsset, quoteAsset, base, quote uint64) {
m.bal[ledgerKey(buyer, quoteAsset)] -= quote
m.bal[ledgerKey(seller, quoteAsset)] += quote
m.bal[ledgerKey(seller, baseAsset)] -= base
m.bal[ledgerKey(buyer, baseAsset)] += base
}
// frameUser returns the 16-byte (UserSize) frame-form user identity for an
// address — the exact bytes the deposit/withdraw frame carries (the proxy
// truncates a 20-byte address to the 16-byte user field; the D-Chain then folds
// THAT to its 8-byte handle). The test keys its ledger by this so it matches the
// matcher's view byte-for-byte.
func frameUser(addr ids.ShortID) []byte { return padUser(userHandle(addr)) }
func balResp(status uint8, amount uint64) []byte {
out := make([]byte, BalanceRespSize)
out[0] = status
binary.BigEndian.PutUint64(out[1:9], amount)
return out
}
// custodyHarness wires a proxy VM with REAL two-chain shared memory and the
// ledger matcher.
type custodyHarness struct {
vm *VM
cChainSM atomic.SharedMemory
proxyChain ids.ID
cChain ids.ID
ledger *ledgerMatcher
}
func newCustodyHarness(t *testing.T) *custodyHarness {
t.Helper()
logger := log.NewNoOpLogger()
baseDB := memdb.New()
memoryDB := prefixdb.New([]byte{0}, baseDB)
m := atomic.NewMemory(memoryDB)
proxyChain := ids.GenerateTestID()
cChain := ids.GenerateTestID()
proxySM := m.NewSharedMemory(proxyChain)
cChainSM := m.NewSharedMemory(cChain)
ledger := newLedgerMatcher()
prev := zapDialer
zapDialer = func(_ context.Context, _ string) (zapConn, error) { return ledger, nil }
t.Cleanup(func() { zapDialer = prev })
rt := &runtime.Runtime{ChainID: proxyChain, CChainID: cChain, NetworkID: 96369, Log: logger, SharedMemory: proxySM}
v := &VM{}
v.Config = config.DefaultConfig()
v.Config.DexZapEndpoint = "127.0.0.1:0"
if err := v.Initialize(context.Background(), vm.Init{
Runtime: rt, DB: prefixdb.New([]byte{1}, baseDB), ToEngine: make(chan vm.Message, 8),
Sender: warp.FakeSender{}, Log: logger,
}); err != nil {
t.Fatalf("init proxy vm: %v", err)
}
v.bootstrapped = true
return &custodyHarness{vm: v, cChainSM: cChainSM, proxyChain: proxyChain, cChain: cChain, ledger: ledger}
}
// fundCChain exports `amount` of `asset` owned by `owner` into shared memory (the
// proxy can claim it on an import), returning the source UTXO id.
func (h *custodyHarness) fundCChain(t *testing.T, owner ids.ShortID, asset ids.ID, amount uint64) ids.ID {
t.Helper()
srcUTXOID := deriveUTXOID(ids.GenerateTestID(), 0)
val := encodeExportedOutput(txs.AtomicOutput{Owner: owner, Asset: asset, Amount: amount})
if err := h.cChainSM.Apply(map[ids.ID]*atomic.Requests{
h.proxyChain: {PutRequests: []*atomic.Element{{Key: srcUTXOID[:], Value: val, Traits: [][]byte{owner[:]}}}},
}); err != nil {
t.Fatalf("fund C-Chain: %v", err)
}
return srcUTXOID
}
// exportedTo sums the value the proxy exported back to C-Chain for an owner.
func (h *custodyHarness) exportedTo(t *testing.T, owner ids.ShortID) uint64 {
t.Helper()
vals, _, _, err := h.cChainSM.Indexed(h.proxyChain, [][]byte{owner[:]}, nil, nil, 100)
if err != nil {
t.Fatalf("Indexed: %v", err)
}
var total uint64
for _, v := range vals {
if len(v) >= 60 {
total += binary.BigEndian.Uint64(v[52:60])
}
}
return total
}
// TestCustodyRailFullCycleConserves is the headline proxy proof: two accounts
// DEPOSIT a non-native asset into the D-Chain via the atomic rail, the D-Chain
// matches them (modeled), and BOTH WITHDRAW — asserting C-Chain value out ==
// C-Chain value in (to the unit) and that the source UTXOs were consumed exactly
// once. This is the maker+taker conservation the prior taker-only model lacked.
func TestCustodyRailFullCycleConserves(t *testing.T) {
h := newCustodyHarness(t)
ctx := context.Background()
maker := ids.GenerateTestShortID()
taker := ids.GenerateTestShortID()
lux := ids.GenerateTestID() // base asset (LUX)
lusd := ids.GenerateTestID() // quote asset (LUSD)
// --- DEPOSIT: maker funds 100 LUX, taker funds 1000 LUSD, atomically. ---
makerUTXO := h.fundCChain(t, maker, lux, 100)
takerUTXO := h.fundCChain(t, taker, lusd, 1000)
arDep := newAtomicRequests()
makerImport := txs.NewImportTx(maker, 0, h.cChain,
[]txs.AtomicInput{{UTXOID: makerUTXO, Asset: lux, Amount: 100}},
[]txs.AtomicOutput{{Owner: maker, Asset: lux, Amount: 100}})
takerImport := txs.NewImportTx(taker, 0, h.cChain,
[]txs.AtomicInput{{UTXOID: takerUTXO, Asset: lusd, Amount: 1000}},
[]txs.AtomicOutput{{Owner: taker, Asset: lusd, Amount: 1000}})
if err := h.vm.executeDeposit(ctx, makerImport, arDep); err != nil {
t.Fatalf("maker deposit: %v", err)
}
if err := h.vm.executeDeposit(ctx, takerImport, arDep); err != nil {
t.Fatalf("taker deposit: %v", err)
}
// Commit the atomic deposit legs (consumes the source UTXOs).
if err := h.vm.commitAtomic(arDep, nil); err != nil {
t.Fatalf("commit deposits: %v", err)
}
// The D-Chain ledger now holds the deposited value.
if got := h.ledger.bal[ledgerKey(frameUser(maker), assetHandle(lux))]; got != 100 {
t.Fatalf("maker LUX in ledger = %d, want 100", got)
}
if got := h.ledger.bal[ledgerKey(frameUser(taker), assetHandle(lusd))]; got != 1000 {
t.Fatalf("taker LUSD in ledger = %d, want 1000", got)
}
// --- TRADE inside the D-Chain: taker buys 10 LUX @ 5 = 50 LUSD. ---
// (The proxy does not trade; the D-Chain matcher does. We model the accepted
// block's settleFills moving balances.)
h.ledger.trade(frameUser(taker), frameUser(maker), assetHandle(lux), assetHandle(lusd), 10, 50)
// Ledger after trade: maker 90 LUX + 50 LUSD ; taker 10 LUX + 950 LUSD.
assertLedger(t, h, maker, lux, 90)
assertLedger(t, h, maker, lusd, 50)
assertLedger(t, h, taker, lux, 10)
assertLedger(t, h, taker, lusd, 950)
// --- WITHDRAW: every account pulls its full realized balance, atomically. ---
arW := newAtomicRequests()
withdrawAll := func(owner ids.ShortID, asset ids.ID, want uint64, idx uint32) uint64 {
r, err := h.vm.executeWithdraw(ctx, owner, asset, want, h.cChain, ids.GenerateTestID(), idx, 1, arW)
if err != nil {
t.Fatalf("withdraw owner=%x asset=%x: %v", owner[:4], asset[:4], err)
}
return r
}
var totalOut uint64
totalOut += withdrawAll(maker, lux, 90, 0)
totalOut += withdrawAll(maker, lusd, 50, 1)
totalOut += withdrawAll(taker, lux, 10, 2)
totalOut += withdrawAll(taker, lusd, 950, 3)
if err := h.vm.commitAtomic(arW, nil); err != nil {
t.Fatalf("commit withdrawals: %v", err)
}
// --- CONSERVATION across the rail ---
const totalIn = 100 + 1000
if totalOut != totalIn {
t.Fatalf("CONSERVATION VIOLATED: withdrawn %d != deposited %d", totalOut, totalIn)
}
// C-Chain can now claim exactly the exported proceeds for each account.
if got := h.exportedTo(t, maker); got != 90+50 {
t.Fatalf("maker C-Chain proceeds = %d, want 140", got)
}
if got := h.exportedTo(t, taker); got != 10+950 {
t.Fatalf("taker C-Chain proceeds = %d, want 960", got)
}
// The ledger is empty — every deposited unit was either traded or withdrawn.
for k, v := range h.ledger.bal {
if v != 0 {
t.Errorf("ledger not drained: key %x has %d", []byte(k), v)
}
}
// Source UTXOs consumed exactly once (double-spend guard held).
for _, u := range []ids.ID{makerUTXO, takerUTXO} {
if c, _ := h.vm.state.IsConsumed(u); !c {
t.Errorf("source UTXO %x not consumed", u[:4])
}
}
}
// TestCustodyWithdrawCannotExceedRealized proves a withdraw exports only the
// ledger's realized (clamped) balance: asking for more than is available exports
// only what is there — the proxy never mints on the export leg.
func TestCustodyWithdrawCannotExceedRealized(t *testing.T) {
h := newCustodyHarness(t)
ctx := context.Background()
owner := ids.GenerateTestShortID()
asset := ids.GenerateTestID()
// Fund + deposit 40.
u := h.fundCChain(t, owner, asset, 40)
ar := newAtomicRequests()
imp := txs.NewImportTx(owner, 0, h.cChain,
[]txs.AtomicInput{{UTXOID: u, Asset: asset, Amount: 40}},
[]txs.AtomicOutput{{Owner: owner, Asset: asset, Amount: 40}})
if err := h.vm.executeDeposit(ctx, imp, ar); err != nil {
t.Fatalf("deposit: %v", err)
}
if err := h.vm.commitAtomic(ar, nil); err != nil {
t.Fatalf("commit: %v", err)
}
// Try to withdraw 100; only 40 is realized.
arW := newAtomicRequests()
realized, err := h.vm.executeWithdraw(ctx, owner, asset, 100, h.cChain, ids.GenerateTestID(), 0, 1, arW)
if err != nil {
t.Fatalf("withdraw: %v", err)
}
if realized != 40 {
t.Fatalf("over-withdraw realized %d, want clamped 40", realized)
}
if err := h.vm.commitAtomic(arW, nil); err != nil {
t.Fatalf("commit withdraw: %v", err)
}
if got := h.exportedTo(t, owner); got != 40 {
t.Fatalf("exported %d, want 40 (no mint)", got)
}
}
// TestCustodyDepositCreditsExactImport proves the deposit rail credits the
// D-Chain ledger with EXACTLY the imported value (no skim, no inflation), and a
// short/over credit from a lying matcher is refused.
func TestCustodyDepositCreditsExactImport(t *testing.T) {
h := newCustodyHarness(t)
ctx := context.Background()
owner := ids.GenerateTestShortID()
asset := ids.GenerateTestID()
u := h.fundCChain(t, owner, asset, 777)
ar := newAtomicRequests()
imp := txs.NewImportTx(owner, 0, h.cChain,
[]txs.AtomicInput{{UTXOID: u, Asset: asset, Amount: 777}},
[]txs.AtomicOutput{{Owner: owner, Asset: asset, Amount: 777}})
if err := h.vm.executeDeposit(ctx, imp, ar); err != nil {
t.Fatalf("deposit: %v", err)
}
if got := h.ledger.bal[ledgerKey(frameUser(owner), assetHandle(asset))]; got != 777 {
t.Fatalf("ledger credited %d, want exactly 777", got)
}
}
func assertLedger(t *testing.T, h *custodyHarness, owner ids.ShortID, asset ids.ID, want uint64) {
t.Helper()
if got := h.ledger.bal[ledgerKey(frameUser(owner), assetHandle(asset))]; got != want {
t.Fatalf("ledger owner=%x asset=%x = %d, want %d", owner[:4], asset[:4], got, want)
}
}
+24
View File
@@ -24,6 +24,30 @@ const (
ZAPMethodPlace = "clob_place"
ZAPMethodCancel = "clob_cancel"
ZAPMethodSubmit = "clob_submit"
// Custody methods — the funds-in / funds-out rail between the atomic
// shared-memory leg and the D-Chain balance ledger (where the money lives in
// the order book). FROZEN, byte-identical with the d-chain gateway
// (github.com/luxfi/dex/pkg/zapwire Method{Deposit,Withdraw,OpenMarket}).
// - clob_deposit : credit an account's available D-Chain balance from value
// this proxy atomically IMPORTED (so the book can draw from it).
// - clob_withdraw: debit an account's realized D-Chain balance; the returned
// realized amount is what this proxy atomically EXPORTS back out.
// - clob_open_market: bind a market's (base,quote) asset handles.
ZAPMethodDeposit = "clob_deposit"
ZAPMethodWithdraw = "clob_withdraw"
ZAPMethodOpenMarket = "clob_open_market"
)
// Custody wire sizes (FROZEN, == github.com/luxfi/dex/pkg/zapwire). The proxy
// re-defines them since it cannot import the cgo-tagged d-chain package; a
// parity test pins byte-equality.
const (
// DepositReqSize / WithdrawReqSize: user[16] + asset[8] + amount[8].
DepositReqSize = 16 + 8 + 8 // 32
WithdrawReqSize = 16 + 8 + 8 // 32
// BalanceRespSize: status[1] + realizedAmount[8].
BalanceRespSize = 1 + 8
)
// FillWireSize is one fill in a clob_submit response: price[8]+size[8]+side[1].