Commit 60e54b41 authored by 贾浩@五瓣科技's avatar 贾浩@五瓣科技

init

parents
Pipeline #830 canceled with stages
.vscode
.idea
build
\ No newline at end of file
.PHONY: default all clean dev api
GOBIN = $(shell pwd)/build
default: api
api:
go build $(BUILD_FLAGS) -v -o=${GOBIN}/$@ ./cmd/api
\ No newline at end of file
package handlers
import (
"net/http"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
"github.com/gin-gonic/gin"
apiTypes "github.com/exchain/orderbook/api/types"
"github.com/exchain/orderbook/engine"
"github.com/exchain/orderbook/orderbook"
"github.com/exchain/orderbook/types"
"github.com/exchain/orderbook/utils"
)
type OrderHandler struct {
eg *engine.Engine
}
func NewOrderHandler(eg *engine.Engine) *OrderHandler {
return &OrderHandler{
eg: eg,
}
}
// PlaceLimitOrder 处理限价单请求
func (h *OrderHandler) PlaceLimitOrder(c *gin.Context) {
var req apiTypes.PlaceLimitOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ok, agent := req.Recover()
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid signature"})
return
}
side := orderbook.Buy
if req.Side == "sell" {
side = orderbook.Sell
}
quantity, err := uint256.FromDecimal(req.Quantity)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid quantity"})
return
}
price, err := uint256.FromDecimal(req.Price)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid price"})
return
}
orderID := utils.GenOrderID()
_, resp, err := h.eg.ProcessLimitOrder(orderID, types.Coin(req.BaseToken), types.Coin(req.QuoteToken), agent, side, price, quantity, req.Nonce)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, resp)
}
// PlaceMarketOrder 处理市价单请求
func (h *OrderHandler) PlaceMarketOrder(c *gin.Context) {
var req apiTypes.PlaceMarketOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ok, agent := req.Recover()
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid signature"})
return
}
side := orderbook.Sell
if req.Side == "buy" {
side = orderbook.Buy
}
quantity, err := uint256.FromDecimal(req.Quantity)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid quantity"})
return
}
orderID := utils.GenOrderID()
_, resp, err := h.eg.ProcessMarketOrder(orderID,
types.Coin(req.BaseToken),
types.Coin(req.QuoteToken),
agent,
side, quantity, req.Nonce)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, resp)
}
// GetOrderBook 获取订单簿深度
func (h *OrderHandler) GetOrderBookDepth(c *gin.Context) {
var req apiTypes.DepthRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
asks, bids, err := h.eg.Depth(types.Coin(req.BaseToken), types.Coin(req.QuoteToken))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"asks": asks,
"bids": bids,
})
}
// CancelOrder 取消订单
func (h *OrderHandler) CancelOrder(c *gin.Context) {
var req apiTypes.CancelOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ok, agent := req.Recover()
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid signature"})
return
}
err := h.eg.ProcessCancelOrder(req.OrderId, types.Coin(req.BaseToken), types.Coin(req.QuoteToken), agent, req.Nonce)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "success",
})
}
func (h *OrderHandler) SignProxy(c *gin.Context) {
var req apiTypes.SignProxyRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ok, user := req.Recover()
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid signature"})
return
}
err := h.eg.PorcessSignProxy(user, common.HexToAddress(req.Agent), req.Nonce)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "success",
})
}
package router
import (
"github.com/exchain/orderbook/api/handlers"
"github.com/gin-gonic/gin"
)
// SetupRouter 配置API路由
func SetupRouter(orderHandler *handlers.OrderHandler) *gin.Engine {
r := gin.Default()
// 订单相关路由
orderGroup := r.Group("/api/v1/orders")
{
orderGroup.POST("/limit", orderHandler.PlaceLimitOrder) // 限价单
orderGroup.POST("/market", orderHandler.PlaceMarketOrder) // 市价单
orderGroup.POST("/cancel", orderHandler.CancelOrder) // 取消订单
}
// 订单簿相关路由
r.GET("/api/v1/orderbook", orderHandler.GetOrderBookDepth) // 获取订单簿深度
return r
}
package types
import (
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/holiman/uint256"
)
// PlaceLimitOrderRequest 限价单请求
type PlaceLimitOrderRequest struct {
BaseToken string `json:"baseToken" binding:"required"`
QuoteToken string `json:"quoteToken" binding:"required"`
Side string `json:"side" binding:"required,oneof=buy sell"`
Quantity string `json:"quantity" binding:"required"`
Price string `json:"price" binding:"required"`
Nonce uint64 `json:"nonce" binding:"required"`
V int32 `json:"v" binding:"required"`
R hexutil.Bytes `json:"r" binding:"required"`
S hexutil.Bytes `json:"s" binding:"required"`
}
func (r *PlaceLimitOrderRequest) Recover() (ok bool, address common.Address) {
data := fmt.Sprintf("baseToken=%s&quoteToken=%s&side=%s&quantity=%s&price=%s&nonce=%d", r.BaseToken, r.QuoteToken, r.Side, r.Quantity, r.Price, r.Nonce)
hashsum := crypto.Keccak256Hash([]byte(data))
// combile sig
sig := make([]byte, 65)
copy(sig, r.R)
copy(sig[32:], r.S)
sig[64] = byte(r.V)
pubkey, err := crypto.SigToPub(hashsum.Bytes(), sig)
if err != nil {
return false, common.Address{}
}
return true, crypto.PubkeyToAddress(*pubkey)
}
// PlaceMarketOrderRequest 市价单请求
type PlaceMarketOrderRequest struct {
BaseToken string `json:"baseToken" binding:"required"`
QuoteToken string `json:"quoteToken" binding:"required"`
Side string `json:"side" binding:"required,oneof=buy sell"`
Quantity string `json:"quantity" binding:"required"`
Nonce uint64 `json:"nonce" binding:"required"`
V int32 `json:"v" binding:"required"`
R hexutil.Bytes `json:"r" binding:"required"`
S hexutil.Bytes `json:"s" binding:"required"`
}
func (r *PlaceMarketOrderRequest) Recover() (ok bool, address common.Address) {
data := fmt.Sprintf("baseToken=%s&quoteToken=%s&side=%s&quantity=%s&nonce=%d", r.BaseToken, r.QuoteToken, r.Side, r.Quantity, r.Nonce)
hashsum := crypto.Keccak256Hash([]byte(data))
// combile sig
sig := make([]byte, 65)
copy(sig, r.R)
copy(sig[32:], r.S)
sig[64] = byte(r.V)
pubkey, err := crypto.SigToPub(hashsum.Bytes(), sig)
if err != nil {
return false, common.Address{}
}
return true, crypto.PubkeyToAddress(*pubkey)
}
type DepthRequest struct {
BaseToken string `json:"baseToken" binding:"required"`
QuoteToken string `json:"quoteToken" binding:"required"`
}
type CancelOrderRequest struct {
BaseToken string `json:"baseToken" binding:"required"`
QuoteToken string `json:"quoteToken" binding:"required"`
OrderId string `json:"orderId" binding:"required"`
Nonce uint64 `json:"nonce" binding:"required"`
V int32 `json:"v" binding:"required"`
R hexutil.Bytes `json:"r" binding:"required"`
S hexutil.Bytes `json:"s" binding:"required"`
}
func (r *CancelOrderRequest) Recover() (ok bool, address common.Address) {
data := fmt.Sprintf("baseToken=%s&quoteToken=%s&orderId=%s&nonce=%d", r.BaseToken, r.QuoteToken, r.OrderId, r.Nonce)
hashsum := crypto.Keccak256Hash([]byte(data))
// combile sig
sig := make([]byte, 65)
copy(sig, r.R)
copy(sig[32:], r.S)
sig[64] = byte(r.V)
pubkey, err := crypto.SigToPub(hashsum.Bytes(), sig)
if err != nil {
return false, common.Address{}
}
return true, crypto.PubkeyToAddress(*pubkey)
}
type SignProxyRequest struct {
Agent string `json:"agent" binding:"required"`
Nonce uint64 `json:"nonce" binding:"required"`
V int32 `json:"v" binding:"required"`
R hexutil.Bytes `json:"r" binding:"required"`
S hexutil.Bytes `json:"s" binding:"required"`
}
func (r *SignProxyRequest) Recover() (ok bool, address common.Address) {
data := fmt.Sprintf("s&agent=%s&nonce=%d", r.Agent, r.Nonce)
hashsum := crypto.Keccak256Hash([]byte(data))
// combile sig
sig := make([]byte, 65)
copy(sig, r.R)
copy(sig[32:], r.S)
sig[64] = byte(r.V)
pubkey, err := crypto.SigToPub(hashsum.Bytes(), sig)
if err != nil {
return false, common.Address{}
}
return true, crypto.PubkeyToAddress(*pubkey)
}
// OrderResponse 订单响应
type OrderResponse struct {
OrderID string `json:"orderId"`
Pair string `json:"pair"`
Side string `json:"side"`
Quantity *uint256.Int `json:"quantity"`
Price *uint256.Int `json:"price"`
Status string `json:"status"`
CreatedAt uint64 `json:"createdAt"`
}
package main
import (
"log"
"github.com/exchain/orderbook/api/handlers"
"github.com/exchain/orderbook/api/router"
"github.com/exchain/orderbook/engine"
)
func main() {
eg := engine.GetEngine()
// 创建订单处理器
orderHandler := handlers.NewOrderHandler(eg)
// 设置路由
r := router.SetupRouter(orderHandler)
// 启动服务器
if err := r.Run(":8080"); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}
package database
import (
"bytes"
"encoding/gob"
"sync"
"github.com/exchain/orderbook/leveldb"
"github.com/exchain/orderbook/types"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
)
type AccountObject struct {
db *leveldb.Database
Address common.Address
Balances map[types.Coin]*uint256.Int
Proxy *common.Address
sync.RWMutex
}
func NewAccountObject(db *leveldb.Database, address common.Address) *AccountObject {
return &AccountObject{
db: db,
Address: address,
Balances: make(map[types.Coin]*uint256.Int),
}
}
func (a *AccountObject) SetProxy(proxy common.Address) {
a.Lock()
defer a.Unlock()
a.Proxy = &proxy
}
func (a *AccountObject) GetProxy() *common.Address {
a.RLock()
defer a.RUnlock()
return a.Proxy
}
func (a *AccountObject) Deposit(coin types.Coin, val *uint256.Int) {
a.AddBalance(coin, val, false)
}
func (a *AccountObject) GetBalance(coin types.Coin, freeze bool) *uint256.Int {
a.RLock()
defer a.RUnlock()
k := coin
if freeze {
k = coin + "_freeze"
}
return a.Balances[k]
}
func (a *AccountObject) Freeze(coin types.Coin, val *uint256.Int) (overflow bool) {
a.Lock()
defer a.Unlock()
of := a.SubBalance(coin, val, false)
if of {
return true
}
a.AddBalance(coin, val, true)
return false
}
func (a *AccountObject) SetBalance(coin types.Coin, balance *uint256.Int, freeze bool) {
a.Lock()
defer a.Unlock()
k := coin
if freeze {
k = coin + "_freeze"
}
a.Balances[k] = balance
}
func (a *AccountObject) SubBalance(coin types.Coin, balance *uint256.Int, freeze bool) (overflow bool) {
a.Lock()
defer a.Unlock()
k := coin
if freeze {
k = coin + "_freeze"
}
newBalance, of := new(uint256.Int).SubOverflow(a.Balances[k], balance)
if of {
return true
}
a.Balances[k] = newBalance
return false
}
func (a *AccountObject) AddBalance(coin types.Coin, balance *uint256.Int, freeze bool) {
a.Lock()
defer a.Unlock()
k := coin
if freeze {
k = coin + "_freeze"
}
newBalance := new(uint256.Int).Add(a.Balances[k], balance)
a.Balances[k] = newBalance
}
func (a *AccountObject) Load(data []byte) error {
decoder := gob.NewDecoder(bytes.NewBuffer(data))
return decoder.Decode(a)
}
func (a *AccountObject) Dump() ([]byte, error) {
var buffer bytes.Buffer
encoder := gob.NewEncoder(&buffer)
if err := encoder.Encode(a); err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
package database
import (
"encoding/json"
"errors"
"fmt"
"github.com/exchain/orderbook/leveldb"
"github.com/exchain/orderbook/orderbook"
"github.com/ethereum/go-ethereum/common"
goleveldb "github.com/syndtr/goleveldb/leveldb"
)
type DexDB struct {
db *leveldb.Database
accountObjects map[common.Address]*AccountObject
}
// New creates a new state from a given trie.
func New(db *leveldb.Database) (*DexDB, error) {
ddb := &DexDB{
db: db,
accountObjects: make(map[common.Address]*AccountObject),
}
return ddb, nil
}
func (ddb *DexDB) GetPairs() (pairs [][]string, err error) {
data, err := ddb.db.Get([]byte("_pairs"))
if err != nil {
return
}
pairs = make([][]string, 0)
err = json.Unmarshal(data, &pairs)
if err != nil {
return
}
return
}
func (ddb *DexDB) GetOrNewAcountObjectByAgent(agent common.Address) (acc *AccountObject, err error) {
data, err := ddb.db.Get([]byte(fmt.Sprintf("proxy_%s", agent.Hex())))
if err != nil && errors.Is(err, goleveldb.ErrNotFound) {
return nil, nil
}
user := common.Address(data)
return ddb.GetOrNewAccountObject(user)
}
func (ddb *DexDB) GetOrNewAccountObject(address common.Address) (acc *AccountObject, err error) {
if acc, ok := ddb.accountObjects[address]; ok {
return acc, nil
}
// get from db
data, err := ddb.db.Get(address.Bytes())
if err != nil && errors.Is(err, goleveldb.ErrNotFound) {
return nil, err
}
if data != nil {
acc = NewAccountObject(ddb.db, address)
if err != nil {
return nil, err
}
err = acc.Load(data)
if err != nil {
return nil, err
}
ddb.accountObjects[address] = acc
return acc, nil
}
acc = NewAccountObject(ddb.db, address)
ddb.accountObjects[address] = acc
return acc, nil
}
func (ddb *DexDB) SaveOrder(order *orderbook.Order) error {
if order == nil {
return nil
}
data, err := order.Dump()
if err != nil {
return err
}
return ddb.db.Put([]byte(order.Id), data)
}
func (ddb *DexDB) SaveAccountObject(acc *AccountObject) error {
if acc == nil {
return nil
}
if acc == nil {
return nil
}
data, err := acc.Dump()
if err != nil {
return err
}
if acc.Proxy != nil {
err = ddb.db.Put([]byte(fmt.Sprintf("proxy_%s", acc.Proxy.Hex())), acc.Address.Bytes())
if err != nil {
return err
}
}
return ddb.db.Put(acc.Address.Bytes(), data)
}
package database
import (
"github.com/exchain/orderbook/leveldb"
"github.com/exchain/orderbook/orderbook"
)
type OrderObject struct {
db *leveldb.Database
Order *orderbook.Order
}
func NewOrderObject(db *leveldb.Database, order *orderbook.Order) *OrderObject {
return &OrderObject{
db: db,
Order: order,
}
}
func (o *OrderObject) Save() error {
data, err := o.Order.Dump()
if err != nil {
return err
}
return o.db.Put([]byte(o.Order.Id), data)
}
func (o *OrderObject) Load() error {
b, err := o.db.Get([]byte(o.Order.Id))
if err != nil {
return err
}
o.Order.Load(b)
return nil
}
package engine
import (
"errors"
"github.com/exchain/orderbook/types"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
)
func (e *Engine) PorcessSignProxy(user common.Address, proxy common.Address, nonce uint64) (err error) {
userObject, err := e.db.GetOrNewAccountObject(user)
if err != nil {
return
}
if userObject == nil {
return errors.New("invalid user")
}
userObject.SetProxy(proxy)
err = e.db.SaveAccountObject(userObject)
if err != nil {
return
}
tx := &types.SignProxyTx{
Tx: types.Tx{
Time: nonce,
User: user,
Action: types.OrderActionSignProxy,
Nonce: nonce,
},
ProxyAddress: proxy,
}
e.AddToQueue(tx)
return
}
func (e *Engine) ProcessDepositBalance(user common.Address, coin types.Coin, amount *uint256.Int) (err error) {
userObject, err := e.db.GetOrNewAccountObject(user)
if err != nil {
return
}
if userObject == nil {
return errors.New("invalid user")
}
userObject.AddBalance(coin, amount, false)
err = e.db.SaveAccountObject(userObject)
if err != nil {
return
}
return
}
package engine
import (
"github.com/exchain/orderbook/database"
"github.com/exchain/orderbook/leveldb"
"github.com/exchain/orderbook/orderbook"
"github.com/exchain/orderbook/types"
"sync"
"github.com/exchain/go-exchain/exchain"
"github.com/exchain/go-exchain/exchain/chaindb"
nebulav1 "github.com/exchain/go-exchain/exchain/protocol/gen/go/nebula/v1"
"github.com/exchain/go-exchain/exchain/wrapper"
)
// Engine 实现订单撮合引擎
type Engine struct {
db *database.DexDB
orderbooks map[string]*orderbook.OrderBook
chainDB chaindb.ChainDB
txQueue chan *nebulav1.Transaction
sync.Mutex
}
var eg *Engine
func GetEngine() *Engine {
if eg == nil {
panic("no engine")
}
return eg
}
// NewEngine 创建一个新的订单撮合引擎
func NewEngine(chainDB chaindb.ChainDB) *Engine {
if eg != nil {
return eg
}
lvdb, err := leveldb.New("data/dexdb", 1024, 1024, "dexdata", false)
if err != nil {
panic(err)
}
db, err := database.New(lvdb)
if err != nil {
panic(err)
}
eg = &Engine{
db: db,
orderbooks: make(map[string]*orderbook.OrderBook),
chainDB: chainDB,
txQueue: make(chan *nebulav1.Transaction, 10240),
}
return eg
}
func (e *Engine) InitPairs() {
pairs, err := e.db.GetPairs()
if err != nil {
panic(err)
}
_defaultPairs := [][]string{
{"BTC", "USDT"},
}
for _, pair := range pairs {
e.orderbooks[string(pair[0]+pair[1])] = orderbook.NewOrderBook(pair[0], pair[1])
}
for _, pair := range _defaultPairs {
e.orderbooks[string(pair[0]+pair[1])] = orderbook.NewOrderBook(pair[0], pair[1])
}
}
func (e *Engine) Start() {
e.InitPairs()
}
func (e *Engine) NewPayload(params exchain.PayloadParams) (*exchain.ExecutionResult, error) {
parent, err := e.chainDB.GetBlockByLabel(chaindb.ExChainBlockLatest)
if err != nil {
return nil, err
}
wParent := wrapper.NewBlkWrapper(parent)
header := &nebulav1.BlockHeader{
Height: parent.Header.Height + 1,
ParentHash: wParent.Hash().Bytes(),
Timestamp: params.Timestamp,
Proposer: params.Proposer.Bytes(),
L1Hash: params.L1Info.BlockHash.Bytes(),
L1Height: params.L1Info.Number,
AppRoot: make([]byte, 0),
}
receipts, err := e.ProcessTx(header, params.Transactions)
if err != nil {
return nil, err
}
orderTxs, orderReceipts, err := e.ProcessOrders(header)
if err != nil {
return nil, err
}
params.Transactions.Txs = append(params.Transactions.Txs, orderTxs.Txs...)
receipts.Receipts = append(receipts.Receipts, orderReceipts.Receipts...)
result := &exchain.ExecutionResult{
Payload: &nebulav1.Block{
Header: header,
Transactions: params.Transactions,
},
Receipts: receipts,
}
return result, nil
}
func (e *Engine) ProcessPayload(block *nebulav1.Block) (exchain.ExecutionResult, error) {
panic("not implemented")
}
func (e *Engine) AddToQueue(tx types.Transaction) {
switch t := tx.(type) {
case *types.PlaceOrderTx:
txType := nebulav1.TxType_MarketTx
if t.LimitPrice != nil {
txType = nebulav1.TxType_LimitTx
}
side := nebulav1.OrderSide_BUY
if t.Action == types.OrderActionSell {
side = nebulav1.OrderSide_SELL
}
ptx := &nebulav1.Transaction{
TxType: txType,
User: t.User.Hex(),
Nonce: t.GetNonce(),
Tx: &nebulav1.Transaction_LimitTx{
LimitTx: &nebulav1.LimitOrderTransaction{
Pair: t.GetPair(),
Side: side,
Quantity: t.GetQuality().Bytes(),
Price: t.GetLimitPrice().Bytes(),
},
},
}
e.txQueue <- ptx
case *types.CancelOrderTx:
ptx := &nebulav1.Transaction{
TxType: nebulav1.TxType_CancelTx,
User: t.User.Hex(),
Nonce: t.GetNonce(),
Tx: &nebulav1.Transaction_CancelTx{
CancelTx: &nebulav1.CancelOrderTransaction{
OrderId: t.GetOrderID(),
},
},
}
e.txQueue <- ptx
case *types.SignProxyTx:
pTx := &nebulav1.Transaction{
TxType: nebulav1.TxType_SignProxyTx,
User: t.User.Hex(),
Nonce: t.GetNonce(),
Tx: &nebulav1.Transaction_SignProxyTx{
SignProxyTx: &nebulav1.SignProxyTransaction{
SignerProxy: t.GetProxyAddress().Bytes(),
},
},
}
e.txQueue <- pTx
default:
panic("not implemented")
}
}
package engine
import (
"errors"
apiTypes "github.com/exchain/orderbook/api/types"
"github.com/exchain/orderbook/orderbook"
"github.com/exchain/orderbook/types"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
)
func (e *Engine) ProcessMarketOrder(orderId string, baseToken, quoteToken types.Coin, agent common.Address, side orderbook.Side, quantity *uint256.Int, nonce uint64) (tx *types.PlaceOrderTx, response *apiTypes.OrderResponse, err error) {
response = new(apiTypes.OrderResponse)
e.Lock()
ob, ok := e.orderbooks[string(baseToken+quoteToken)]
e.Unlock()
if !ok {
err = errors.New("invalid pair")
return
}
makerObj, err := e.db.GetOrNewAcountObjectByAgent(agent)
if err != nil {
return
}
if makerObj == nil {
err = errors.New("invalid agent")
return
}
tx = new(types.PlaceOrderTx)
tx.OrderID = orderId
tx.Nonce = nonce
tx.Time = nonce
tx.User = makerObj.Address
tx.BaseCoin = baseToken
tx.QuoteCoin = quoteToken
tx.Type = types.PlaceOrder
if side == orderbook.Sell {
tx.Action = types.OrderActionSell
} else {
tx.Action = types.OrderActionBuy
}
tx.LimitPrice = nil
tx.Quantity = quantity
makerOrder := orderbook.NewOrder(orderId, makerObj.Address, side, quantity, uint256.NewInt(0), nonce)
err = e.db.SaveOrder(makerOrder)
if err != nil {
return
}
done, partial, partialQty, quantityLeft, err := ob.ProcessMarketOrder(side, quantity)
if err != nil {
return
}
totalQuantity := uint256.NewInt(0)
totalValue := uint256.NewInt(0)
for _, order := range done {
totalQuantity = new(uint256.Int).Add(totalQuantity, order.Quantity)
value := new(uint256.Int).Mul(order.Price, order.Quantity)
totalValue = new(uint256.Int).Add(totalValue, value)
err = e.db.SaveOrder(order)
if err != nil {
return nil, nil, err
}
makerOrder.Fill(order.Quantity)
takerObj, err := e.db.GetOrNewAccountObject(order.Creator)
if err != nil {
return nil, nil, err
}
if order.Side == orderbook.Sell {
takerObj.SubBalance(baseToken, order.Quantity, true) // 市价单对手只能是限价单,减去对手的冻结金额
takerObj.AddBalance(quoteToken, value, false) // 对手加quoteToken
makerObj.SubBalance(quoteToken, value, false) // 直接减去流动金额
makerObj.AddBalance(baseToken, order.Quantity, false) // 对手加baseToken
}
if order.Side == orderbook.Buy {
takerObj.SubBalance(quoteToken, order.Quantity, true)
takerObj.AddBalance(baseToken, value, false)
makerObj.SubBalance(baseToken, order.Quantity, false)
makerObj.AddBalance(quoteToken, value, false)
}
}
if partial != nil {
totalQuantity = new(uint256.Int).Add(totalQuantity, partialQty)
value := new(uint256.Int).Mul(partial.Price, partialQty)
totalValue = new(uint256.Int).Add(totalValue, value)
err = e.db.SaveOrder(partial)
if err != nil {
return nil, nil, err
}
makerOrder.Fill(partialQty)
takerObj, err := e.db.GetOrNewAccountObject(partial.Creator)
if err != nil {
return nil, nil, err
}
if partial.Side == orderbook.Sell {
takerObj.SubBalance(baseToken, partialQty, true)
takerObj.AddBalance(quoteToken, value, false)
makerObj.SubBalance(quoteToken, value, false)
makerObj.AddBalance(baseToken, partialQty, false)
}
if partial.Side == orderbook.Buy {
takerObj.SubBalance(quoteToken, partialQty, true)
takerObj.AddBalance(baseToken, value, false)
makerObj.SubBalance(baseToken, value, false)
makerObj.AddBalance(quoteToken, partialQty, false)
}
}
avgPrice := uint256.NewInt(0)
if totalQuantity.Cmp(uint256.NewInt(0)) > 0 {
avgPrice = new(uint256.Int).Div(totalValue, totalQuantity)
}
makerOrder.AvgPrice = avgPrice
err = e.db.SaveOrder(makerOrder)
if err != nil {
return nil, nil, err
}
e.AddToQueue(tx)
response = &apiTypes.OrderResponse{
OrderID: makerOrder.Id,
Side: string(tx.Action),
Quantity: new(uint256.Int).Sub(quantity, quantityLeft),
Price: avgPrice,
CreatedAt: nonce,
}
if quantityLeft.Cmp(uint256.NewInt(0)) > 0 {
response.Status = "partial"
} else {
response.Status = "filled"
}
return tx, response, nil
}
func (e *Engine) ProcessLimitOrder(orderId string, baseToken, quoteToken types.Coin, agent common.Address, side orderbook.Side, price, quantity *uint256.Int, nonce uint64) (tx *types.PlaceOrderTx, response *apiTypes.OrderResponse, err error) {
response = new(apiTypes.OrderResponse)
e.Lock()
ob, ok := e.orderbooks[string(baseToken+quoteToken)]
e.Unlock()
if !ok {
err = errors.New("invalid pair")
return
}
makerObj, err := e.db.GetOrNewAcountObjectByAgent(agent)
if err != nil {
return
}
if makerObj == nil {
err = errors.New("invalid agent")
return
}
tx = new(types.PlaceOrderTx)
tx.OrderID = orderId
tx.Nonce = nonce
tx.Time = nonce
tx.User = makerObj.Address
tx.BaseCoin = baseToken
tx.QuoteCoin = quoteToken
tx.Type = types.PlaceOrder
if side == orderbook.Sell {
tx.Action = types.OrderActionSell
} else {
tx.Action = types.OrderActionBuy
}
tx.LimitPrice = price
tx.Quantity = quantity
currentBaseBalance := makerObj.GetBalance(baseToken, false)
currentQuoteBalance := makerObj.GetBalance(quoteToken, false)
if side == orderbook.Sell {
if currentBaseBalance.Cmp(quantity) < 0 {
err = errors.New("insufficient balance")
return
}
// 冻结金额
makerObj.SubBalance(baseToken, quantity, false)
makerObj.AddBalance(baseToken, quantity, true)
} else {
if currentQuoteBalance.Cmp(quantity) < 0 {
err = errors.New("insufficient balance")
return
}
// 计算需要消耗多少quoteToken,再冻结
value := new(uint256.Int).Mul(price, quantity)
makerObj.SubBalance(quoteToken, value, false)
makerObj.AddBalance(quoteToken, value, true)
}
done, partial, _, partialQty, quantityLeft, err := ob.ProcessLimitOrder(side, orderId, makerObj.Address, quantity, price, nonce)
if err != nil {
return
}
for _, order := range done {
value := new(uint256.Int).Mul(order.Price, order.Quantity)
err = e.db.SaveOrder(order)
if err != nil {
return nil, nil, err
}
takerObj, err := e.db.GetOrNewAccountObject(order.Creator)
if err != nil {
return nil, nil, err
}
if order.Side == orderbook.Sell {
takerObj.SubBalance(baseToken, order.Quantity, true)
takerObj.AddBalance(quoteToken, value, false)
makerObj.SubBalance(quoteToken, value, true)
makerObj.AddBalance(baseToken, order.Quantity, false)
}
if order.Side == orderbook.Buy {
takerObj.SubBalance(quoteToken, order.Quantity, true)
takerObj.AddBalance(baseToken, value, false)
makerObj.SubBalance(baseToken, order.Quantity, true)
makerObj.AddBalance(quoteToken, value, false)
}
}
// != 说明完全成交了,但要更新对手最后半个订单的状态
if partial != nil && partial.Creator.Hex() != makerObj.Address.Hex() {
value := new(uint256.Int).Mul(partial.Price, partialQty)
err = e.db.SaveOrder(partial)
if err != nil {
return nil, nil, err
}
takerObj, err := e.db.GetOrNewAccountObject(partial.Creator)
if err != nil {
return nil, nil, err
}
if partial.Side == orderbook.Sell {
takerObj.SubBalance(baseToken, partialQty, true)
takerObj.AddBalance(quoteToken, value, false)
makerObj.SubBalance(quoteToken, value, true)
makerObj.AddBalance(baseToken, partialQty, false)
}
if partial.Side == orderbook.Buy {
takerObj.SubBalance(quoteToken, partialQty, true)
takerObj.AddBalance(baseToken, value, false)
makerObj.SubBalance(baseToken, partialQty, true)
makerObj.AddBalance(quoteToken, value, false)
}
}
response.Status = "filled"
// == 说明吃了所有的单也没有完全成交,假设一点也没成交,不需要关心余额变化
if partial != nil && partial.Creator.Hex() == makerObj.Address.Hex() {
err = e.db.SaveOrder(partial)
if err != nil {
return nil, nil, err
}
response.Status = "partial"
}
avgPrice := price
e.AddToQueue(tx)
response = &apiTypes.OrderResponse{
OrderID: orderId,
Side: string(tx.Action),
Quantity: new(uint256.Int).Sub(quantity, quantityLeft),
Price: avgPrice,
CreatedAt: nonce,
}
return tx, response, nil
}
func (e *Engine) ProcessCancelOrder(orderId string, baseToken, quoteToken types.Coin, agent common.Address, nonce uint64) (err error) {
e.Lock()
ob, ok := e.orderbooks[string(baseToken+quoteToken)]
e.Unlock()
if !ok {
return
}
makerObj, err := e.db.GetOrNewAcountObjectByAgent(agent)
if err != nil {
return
}
if makerObj == nil {
return errors.New("invalid agent")
}
order := ob.Order(orderId)
if order == nil {
return errors.New("invalid order id")
}
if order.Creator.Hex() != makerObj.Address.Hex() {
return errors.New("not owner")
}
_, err = ob.CancelOrder(orderId)
if err != nil {
return
}
tx := &types.CancelOrderTx{
Tx: types.Tx{
OrderID: orderId,
Time: nonce,
User: makerObj.Address,
Action: types.OrderActionCancel,
Nonce: nonce,
},
}
e.AddToQueue(tx)
return nil
}
func (e *Engine) Depth(baseToken, quoteToken types.Coin) (asks, bids []*orderbook.PriceLevel, err error) {
e.Lock()
ob, ok := e.orderbooks[string(baseToken+quoteToken)]
e.Unlock()
if !ok {
err = errors.New("invalid pair")
return
}
a, b := ob.Depth()
return a, b, nil
}
package engine
import (
"errors"
"github.com/exchain/orderbook/types"
"github.com/ethereum/go-ethereum/common"
nebulav1 "github.com/exchain/go-exchain/exchain/protocol/gen/go/nebula/v1"
"github.com/exchain/go-exchain/exchain/wrapper"
"github.com/holiman/uint256"
)
func (e *Engine) ProcessOrders(header *nebulav1.BlockHeader) (txs *nebulav1.TransactionList, receipts *nebulav1.TransactionReceiptList, err error) {
txs = &nebulav1.TransactionList{}
txs.Txs = make([]*nebulav1.Transaction, 0)
receipts = &nebulav1.TransactionReceiptList{}
receipts.Receipts = make([]*nebulav1.TransactionReceipt, 0)
outer:
for i := 0; i < 100 && len(txs.Txs) < 100; i++ {
select {
case tx := <-e.txQueue:
if tx == nil {
break
}
txs.Txs = append(txs.Txs, tx)
default:
break outer
}
}
for _, tx := range txs.Txs {
wtx := wrapper.NewTxWrapper(tx)
switch tx.TxType {
case nebulav1.TxType_LimitTx:
receipts.Receipts = append(receipts.Receipts, &nebulav1.TransactionReceipt{
Hash: wtx.Hash().Bytes(),
TxType: tx.TxType,
Success: true,
BlockHeight: header.Height,
Timestamp: header.Timestamp,
Content: &nebulav1.TransactionReceipt_LimitR{
LimitR: &nebulav1.LimitOrderReceipt{},
},
})
case nebulav1.TxType_MarketTx:
receipts.Receipts = append(receipts.Receipts, &nebulav1.TransactionReceipt{
Hash: wtx.Hash().Bytes(),
TxType: tx.TxType,
Success: true,
BlockHeight: header.Height,
Timestamp: header.Timestamp,
Content: &nebulav1.TransactionReceipt_MarketR{
MarketR: &nebulav1.MarketOrderReceipt{},
},
})
case nebulav1.TxType_CancelTx:
receipts.Receipts = append(receipts.Receipts, &nebulav1.TransactionReceipt{
Hash: wtx.Hash().Bytes(),
TxType: tx.TxType,
Success: true,
BlockHeight: header.Height,
Timestamp: header.Timestamp,
Content: &nebulav1.TransactionReceipt_CancelR{
CancelR: &nebulav1.CancelOrderReceipt{},
},
})
case nebulav1.TxType_SignProxyTx:
receipts.Receipts = append(receipts.Receipts, &nebulav1.TransactionReceipt{
Hash: wtx.Hash().Bytes(),
TxType: tx.TxType,
Success: true,
BlockHeight: header.Height,
Timestamp: header.Timestamp,
Content: &nebulav1.TransactionReceipt_SignProxyR{
SignProxyR: &nebulav1.SignProxyReceipt{},
},
})
default:
panic("not implemented")
}
}
return txs, receipts, nil
}
func (e *Engine) ProcessTx(header *nebulav1.BlockHeader, txs *nebulav1.TransactionList) (*nebulav1.TransactionReceiptList, error) {
receipts := &nebulav1.TransactionReceiptList{
Receipts: make([]*nebulav1.TransactionReceipt, 0),
}
for _, tx := range txs.Txs {
wtx := wrapper.NewTxWrapper(tx)
receipt := &nebulav1.TransactionReceipt{
Hash: wtx.Hash().Bytes(),
TxType: tx.TxType,
Success: true,
BlockHeight: header.Height,
Timestamp: header.Timestamp,
}
switch tx.TxType {
case nebulav1.TxType_DepositTx:
address := common.Address(tx.Tx.(*nebulav1.Transaction_DepositTx).DepositTx.User)
coin := types.Coin(tx.Tx.(*nebulav1.Transaction_DepositTx).DepositTx.Coin)
amount := tx.Tx.(*nebulav1.Transaction_DepositTx).DepositTx.Amount
amt := new(uint256.Int).SetBytes(amount)
err := e.ProcessDepositBalance(address, coin, amt)
if err != nil {
receipt.Success = false
}
receipt.Content = &nebulav1.TransactionReceipt_DepositR{
DepositR: &nebulav1.DepositReceipt{},
}
case nebulav1.TxType_LimitTx:
receipt.Content = &nebulav1.TransactionReceipt_LimitR{
LimitR: &nebulav1.LimitOrderReceipt{},
}
case nebulav1.TxType_WithdrawTx:
receipt.Content = &nebulav1.TransactionReceipt_WithdrawR{
WithdrawR: &nebulav1.WithdrawReceipt{},
}
case nebulav1.TxType_CancelTx:
receipt.Content = &nebulav1.TransactionReceipt_CancelR{
CancelR: &nebulav1.CancelOrderReceipt{},
}
case nebulav1.TxType_MarketTx:
receipt.Content = &nebulav1.TransactionReceipt_MarketR{
MarketR: &nebulav1.MarketOrderReceipt{},
}
case nebulav1.TxType_CreatePairTx:
err := e.ProcessCreatePair(tx)
if err != nil {
receipt.Success = false
}
receipt.Content = &nebulav1.TransactionReceipt_CreatePairR{
CreatePairR: &nebulav1.CreatePairReceipt{},
}
case nebulav1.TxType_DisablePairTx:
err := e.ProcessDisablePair(tx)
if err != nil {
receipt.Success = false
}
receipt.Content = &nebulav1.TransactionReceipt_DisablePairR{
DisablePairR: &nebulav1.DisablePairReceipt{},
}
case nebulav1.TxType_ProtocolTx:
receipt.Content = &nebulav1.TransactionReceipt_ProtocolR{
ProtocolR: &nebulav1.ProtocolTransactionReceipt{},
}
case nebulav1.TxType_SignProxyTx:
receipt.Content = &nebulav1.TransactionReceipt_SignProxyR{
SignProxyR: &nebulav1.SignProxyReceipt{},
}
default:
// TODO: return error
return receipts, errors.New("not implemented")
}
receipts.Receipts = append(receipts.Receipts, receipt)
}
return receipts, nil
}
package engine
import nebulav1 "github.com/exchain/go-exchain/exchain/protocol/gen/go/nebula/v1"
func (e *Engine) ProcessCreatePair(tx *nebulav1.Transaction) error {
panic("not implemented")
}
func (e *Engine) ProcessDisablePair(tx *nebulav1.Transaction) error {
panic("not implemented")
}
module github.com/exchain/orderbook
go 1.23.3
require (
github.com/bwmarrin/snowflake v0.3.0
github.com/emirpasic/gods v1.18.1
github.com/ethereum/go-ethereum v1.15.0
github.com/exchain/go-exchain v0.0.1
github.com/gin-gonic/gin v1.10.0
github.com/holiman/uint256 v1.3.2
github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a
)
require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru v0.5.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/oklog/ulid/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
)
require (
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/exchain/go-exchain v0.0.1 => ../nebula/
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
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=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/ethereum/go-ethereum v1.15.0 h1:LLb2jCPsbJZcB4INw+E/MgzUX5wlR6SdwXcv09/1ME4=
github.com/ethereum/go-ethereum v1.15.0/go.mod h1:4q+4t48P2C03sjqGvTXix5lEOplf5dz4CTosbjt5tGs=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
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/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
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.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=
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI=
github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
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=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
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=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
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=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-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=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.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=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
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.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=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
// Copyright 2018 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
//go:build !js && !wasip1
// +build !js,!wasip1
// Package leveldb implements the key-value database layer based on LevelDB.
package leveldb
import (
"bytes"
"fmt"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/errors"
"github.com/syndtr/goleveldb/leveldb/filter"
"github.com/syndtr/goleveldb/leveldb/opt"
"github.com/syndtr/goleveldb/leveldb/util"
)
const (
// degradationWarnInterval specifies how often warning should be printed if the
// leveldb database cannot keep up with requested writes.
degradationWarnInterval = time.Minute
// minCache is the minimum amount of memory in megabytes to allocate to leveldb
// read and write caching, split half and half.
minCache = 16
// minHandles is the minimum number of files handles to allocate to the open
// database files.
minHandles = 16
// metricsGatheringInterval specifies the interval to retrieve leveldb database
// compaction, io and pause stats to report to the user.
metricsGatheringInterval = 3 * time.Second
)
// Database is a persistent key-value store. Apart from basic data storage
// functionality it also supports batch writes and iterating over the keyspace in
// binary-alphabetical order.
type Database struct {
fn string // filename for reporting
db *leveldb.DB // LevelDB instance
compTimeMeter *metrics.Meter // Meter for measuring the total time spent in database compaction
compReadMeter *metrics.Meter // Meter for measuring the data read during compaction
compWriteMeter *metrics.Meter // Meter for measuring the data written during compaction
writeDelayNMeter *metrics.Meter // Meter for measuring the write delay number due to database compaction
writeDelayMeter *metrics.Meter // Meter for measuring the write delay duration due to database compaction
diskSizeGauge *metrics.Gauge // Gauge for tracking the size of all the levels in the database
diskReadMeter *metrics.Meter // Meter for measuring the effective amount of data read
diskWriteMeter *metrics.Meter // Meter for measuring the effective amount of data written
memCompGauge *metrics.Gauge // Gauge for tracking the number of memory compaction
level0CompGauge *metrics.Gauge // Gauge for tracking the number of table compaction in level0
nonlevel0CompGauge *metrics.Gauge // Gauge for tracking the number of table compaction in non0 level
seekCompGauge *metrics.Gauge // Gauge for tracking the number of table compaction caused by read opt
manualMemAllocGauge *metrics.Gauge // Gauge to track the amount of memory that has been manually allocated (not a part of runtime/GC)
levelsGauge []*metrics.Gauge // Gauge for tracking the number of tables in levels
quitLock sync.Mutex // Mutex protecting the quit channel access
quitChan chan chan error // Quit channel to stop the metrics collection before closing the database
log log.Logger // Contextual logger tracking the database path
}
// New returns a wrapped LevelDB object. The namespace is the prefix that the
// metrics reporting should use for surfacing internal stats.
func New(file string, cache int, handles int, namespace string, readonly bool) (*Database, error) {
return NewCustom(file, namespace, func(options *opt.Options) {
// Ensure we have some minimal caching and file guarantees
if cache < minCache {
cache = minCache
}
if handles < minHandles {
handles = minHandles
}
// Set default options
options.OpenFilesCacheCapacity = handles
options.BlockCacheCapacity = cache / 2 * opt.MiB
options.WriteBuffer = cache / 4 * opt.MiB // Two of these are used internally
if readonly {
options.ReadOnly = true
}
})
}
// NewCustom returns a wrapped LevelDB object. The namespace is the prefix that the
// metrics reporting should use for surfacing internal stats.
// The customize function allows the caller to modify the leveldb options.
func NewCustom(file string, namespace string, customize func(options *opt.Options)) (*Database, error) {
options := configureOptions(customize)
logger := log.New("database", file)
usedCache := options.GetBlockCacheCapacity() + options.GetWriteBuffer()*2
logCtx := []interface{}{"cache", common.StorageSize(usedCache), "handles", options.GetOpenFilesCacheCapacity()}
if options.ReadOnly {
logCtx = append(logCtx, "readonly", "true")
}
logger.Info("Allocated cache and file handles", logCtx...)
// Open the db and recover any potential corruptions
db, err := leveldb.OpenFile(file, options)
if _, corrupted := err.(*errors.ErrCorrupted); corrupted {
db, err = leveldb.RecoverFile(file, nil)
}
if err != nil {
return nil, err
}
// Assemble the wrapper with all the registered metrics
ldb := &Database{
fn: file,
db: db,
log: logger,
quitChan: make(chan chan error),
}
ldb.compTimeMeter = metrics.NewRegisteredMeter(namespace+"compact/time", nil)
ldb.compReadMeter = metrics.NewRegisteredMeter(namespace+"compact/input", nil)
ldb.compWriteMeter = metrics.NewRegisteredMeter(namespace+"compact/output", nil)
ldb.diskSizeGauge = metrics.NewRegisteredGauge(namespace+"disk/size", nil)
ldb.diskReadMeter = metrics.NewRegisteredMeter(namespace+"disk/read", nil)
ldb.diskWriteMeter = metrics.NewRegisteredMeter(namespace+"disk/write", nil)
ldb.writeDelayMeter = metrics.NewRegisteredMeter(namespace+"compact/writedelay/duration", nil)
ldb.writeDelayNMeter = metrics.NewRegisteredMeter(namespace+"compact/writedelay/counter", nil)
ldb.memCompGauge = metrics.NewRegisteredGauge(namespace+"compact/memory", nil)
ldb.level0CompGauge = metrics.NewRegisteredGauge(namespace+"compact/level0", nil)
ldb.nonlevel0CompGauge = metrics.NewRegisteredGauge(namespace+"compact/nonlevel0", nil)
ldb.seekCompGauge = metrics.NewRegisteredGauge(namespace+"compact/seek", nil)
ldb.manualMemAllocGauge = metrics.NewRegisteredGauge(namespace+"memory/manualalloc", nil)
// Start up the metrics gathering and return
go ldb.meter(metricsGatheringInterval, namespace)
return ldb, nil
}
// configureOptions sets some default options, then runs the provided setter.
func configureOptions(customizeFn func(*opt.Options)) *opt.Options {
// Set default options
options := &opt.Options{
Filter: filter.NewBloomFilter(10),
DisableSeeksCompaction: true,
}
// Allow caller to make custom modifications to the options
if customizeFn != nil {
customizeFn(options)
}
return options
}
// Close stops the metrics collection, flushes any pending data to disk and closes
// all io accesses to the underlying key-value store.
func (db *Database) Close() error {
db.quitLock.Lock()
defer db.quitLock.Unlock()
if db.quitChan != nil {
errc := make(chan error)
db.quitChan <- errc
if err := <-errc; err != nil {
db.log.Error("Metrics collection failed", "err", err)
}
db.quitChan = nil
}
return db.db.Close()
}
// Has retrieves if a key is present in the key-value store.
func (db *Database) Has(key []byte) (bool, error) {
return db.db.Has(key, nil)
}
// Get retrieves the given key if it's present in the key-value store.
func (db *Database) Get(key []byte) ([]byte, error) {
dat, err := db.db.Get(key, nil)
if err != nil {
return nil, err
}
return dat, nil
}
// Put inserts the given value into the key-value store.
func (db *Database) Put(key []byte, value []byte) error {
return db.db.Put(key, value, nil)
}
// Delete removes the key from the key-value store.
func (db *Database) Delete(key []byte) error {
return db.db.Delete(key, nil)
}
var ErrTooManyKeys = errors.New("too many keys in deleted range")
// DeleteRange deletes all of the keys (and values) in the range [start,end)
// (inclusive on start, exclusive on end).
// Note that this is a fallback implementation as leveldb does not natively
// support range deletion. It can be slow and therefore the number of deleted
// keys is limited in order to avoid blocking for a very long time.
// ErrTooManyKeys is returned if the range has only been partially deleted.
// In this case the caller can repeat the call until it finally succeeds.
func (db *Database) DeleteRange(start, end []byte) error {
batch := db.NewBatch()
it := db.NewIterator(nil, start)
defer it.Release()
var count int
for it.Next() && bytes.Compare(end, it.Key()) > 0 {
count++
if count > 10000 { // should not block for more than a second
if err := batch.Write(); err != nil {
return err
}
return ErrTooManyKeys
}
if err := batch.Delete(it.Key()); err != nil {
return err
}
}
return batch.Write()
}
// NewBatch creates a write-only key-value store that buffers changes to its host
// database until a final write is called.
func (db *Database) NewBatch() ethdb.Batch {
return &batch{
db: db.db,
b: new(leveldb.Batch),
}
}
// NewBatchWithSize creates a write-only database batch with pre-allocated buffer.
func (db *Database) NewBatchWithSize(size int) ethdb.Batch {
return &batch{
db: db.db,
b: leveldb.MakeBatch(size),
}
}
// NewIterator creates a binary-alphabetical iterator over a subset
// of database content with a particular key prefix, starting at a particular
// initial key (or after, if it does not exist).
func (db *Database) NewIterator(prefix []byte, start []byte) ethdb.Iterator {
return db.db.NewIterator(bytesPrefixRange(prefix, start), nil)
}
// Stat returns the statistic data of the database.
func (db *Database) Stat() (string, error) {
var stats leveldb.DBStats
if err := db.db.Stats(&stats); err != nil {
return "", err
}
var (
message string
totalRead int64
totalWrite int64
totalSize int64
totalTables int
totalDuration time.Duration
)
if len(stats.LevelSizes) > 0 {
message += " Level | Tables | Size(MB) | Time(sec) | Read(MB) | Write(MB)\n" +
"-------+------------+---------------+---------------+---------------+---------------\n"
for level, size := range stats.LevelSizes {
read := stats.LevelRead[level]
write := stats.LevelWrite[level]
duration := stats.LevelDurations[level]
tables := stats.LevelTablesCounts[level]
if tables == 0 && duration == 0 {
continue
}
totalTables += tables
totalSize += size
totalRead += read
totalWrite += write
totalDuration += duration
message += fmt.Sprintf(" %3d | %10d | %13.5f | %13.5f | %13.5f | %13.5f\n",
level, tables, float64(size)/1048576.0, duration.Seconds(),
float64(read)/1048576.0, float64(write)/1048576.0)
}
message += "-------+------------+---------------+---------------+---------------+---------------\n"
message += fmt.Sprintf(" Total | %10d | %13.5f | %13.5f | %13.5f | %13.5f\n",
totalTables, float64(totalSize)/1048576.0, totalDuration.Seconds(),
float64(totalRead)/1048576.0, float64(totalWrite)/1048576.0)
message += "-------+------------+---------------+---------------+---------------+---------------\n\n"
}
message += fmt.Sprintf("Read(MB):%.5f Write(MB):%.5f\n", float64(stats.IORead)/1048576.0, float64(stats.IOWrite)/1048576.0)
message += fmt.Sprintf("BlockCache(MB):%.5f FileCache:%d\n", float64(stats.BlockCacheSize)/1048576.0, stats.OpenedTablesCount)
message += fmt.Sprintf("MemoryCompaction:%d Level0Compaction:%d NonLevel0Compaction:%d SeekCompaction:%d\n", stats.MemComp, stats.Level0Comp, stats.NonLevel0Comp, stats.SeekComp)
message += fmt.Sprintf("WriteDelayCount:%d WriteDelayDuration:%s Paused:%t\n", stats.WriteDelayCount, common.PrettyDuration(stats.WriteDelayDuration), stats.WritePaused)
message += fmt.Sprintf("Snapshots:%d Iterators:%d\n", stats.AliveSnapshots, stats.AliveIterators)
return message, nil
}
// Compact flattens the underlying data store for the given key range. In essence,
// deleted and overwritten versions are discarded, and the data is rearranged to
// reduce the cost of operations needed to access them.
//
// A nil start is treated as a key before all keys in the data store; a nil limit
// is treated as a key after all keys in the data store. If both is nil then it
// will compact entire data store.
func (db *Database) Compact(start []byte, limit []byte) error {
return db.db.CompactRange(util.Range{Start: start, Limit: limit})
}
// Path returns the path to the database directory.
func (db *Database) Path() string {
return db.fn
}
// meter periodically retrieves internal leveldb counters and reports them to
// the metrics subsystem.
func (db *Database) meter(refresh time.Duration, namespace string) {
// Create the counters to store current and previous compaction values
compactions := make([][]int64, 2)
for i := 0; i < 2; i++ {
compactions[i] = make([]int64, 4)
}
// Create storages for states and warning log tracer.
var (
errc chan error
merr error
stats leveldb.DBStats
iostats [2]int64
delaystats [2]int64
lastWritePaused time.Time
)
timer := time.NewTimer(refresh)
defer timer.Stop()
// Iterate ad infinitum and collect the stats
for i := 1; errc == nil && merr == nil; i++ {
// Retrieve the database stats
// Stats method resets buffers inside therefore it's okay to just pass the struct.
err := db.db.Stats(&stats)
if err != nil {
db.log.Error("Failed to read database stats", "err", err)
merr = err
continue
}
// Iterate over all the leveldbTable rows, and accumulate the entries
for j := 0; j < len(compactions[i%2]); j++ {
compactions[i%2][j] = 0
}
compactions[i%2][0] = stats.LevelSizes.Sum()
for _, t := range stats.LevelDurations {
compactions[i%2][1] += t.Nanoseconds()
}
compactions[i%2][2] = stats.LevelRead.Sum()
compactions[i%2][3] = stats.LevelWrite.Sum()
// Update all the requested meters
if db.diskSizeGauge != nil {
db.diskSizeGauge.Update(compactions[i%2][0])
}
if db.compTimeMeter != nil {
db.compTimeMeter.Mark(compactions[i%2][1] - compactions[(i-1)%2][1])
}
if db.compReadMeter != nil {
db.compReadMeter.Mark(compactions[i%2][2] - compactions[(i-1)%2][2])
}
if db.compWriteMeter != nil {
db.compWriteMeter.Mark(compactions[i%2][3] - compactions[(i-1)%2][3])
}
var (
delayN = int64(stats.WriteDelayCount)
duration = stats.WriteDelayDuration
paused = stats.WritePaused
)
if db.writeDelayNMeter != nil {
db.writeDelayNMeter.Mark(delayN - delaystats[0])
}
if db.writeDelayMeter != nil {
db.writeDelayMeter.Mark(duration.Nanoseconds() - delaystats[1])
}
// If a warning that db is performing compaction has been displayed, any subsequent
// warnings will be withheld for one minute not to overwhelm the user.
if paused && delayN-delaystats[0] == 0 && duration.Nanoseconds()-delaystats[1] == 0 &&
time.Now().After(lastWritePaused.Add(degradationWarnInterval)) {
db.log.Warn("Database compacting, degraded performance")
lastWritePaused = time.Now()
}
delaystats[0], delaystats[1] = delayN, duration.Nanoseconds()
var (
nRead = int64(stats.IORead)
nWrite = int64(stats.IOWrite)
)
if db.diskReadMeter != nil {
db.diskReadMeter.Mark(nRead - iostats[0])
}
if db.diskWriteMeter != nil {
db.diskWriteMeter.Mark(nWrite - iostats[1])
}
iostats[0], iostats[1] = nRead, nWrite
db.memCompGauge.Update(int64(stats.MemComp))
db.level0CompGauge.Update(int64(stats.Level0Comp))
db.nonlevel0CompGauge.Update(int64(stats.NonLevel0Comp))
db.seekCompGauge.Update(int64(stats.SeekComp))
for i, tables := range stats.LevelTablesCounts {
// Append metrics for additional layers
if i >= len(db.levelsGauge) {
db.levelsGauge = append(db.levelsGauge, metrics.NewRegisteredGauge(namespace+fmt.Sprintf("tables/level%v", i), nil))
}
db.levelsGauge[i].Update(int64(tables))
}
// Sleep a bit, then repeat the stats collection
select {
case errc = <-db.quitChan:
// Quit requesting, stop hammering the database
case <-timer.C:
timer.Reset(refresh)
// Timeout, gather a new set of stats
}
}
if errc == nil {
errc = <-db.quitChan
}
errc <- merr
}
// batch is a write-only leveldb batch that commits changes to its host database
// when Write is called. A batch cannot be used concurrently.
type batch struct {
db *leveldb.DB
b *leveldb.Batch
size int
}
// Put inserts the given value into the batch for later committing.
func (b *batch) Put(key, value []byte) error {
b.b.Put(key, value)
b.size += len(key) + len(value)
return nil
}
// Delete inserts the key removal into the batch for later committing.
func (b *batch) Delete(key []byte) error {
b.b.Delete(key)
b.size += len(key)
return nil
}
// ValueSize retrieves the amount of data queued up for writing.
func (b *batch) ValueSize() int {
return b.size
}
// Write flushes any accumulated data to disk.
func (b *batch) Write() error {
return b.db.Write(b.b, nil)
}
// Reset resets the batch for reuse.
func (b *batch) Reset() {
b.b.Reset()
b.size = 0
}
// Replay replays the batch contents.
func (b *batch) Replay(w ethdb.KeyValueWriter) error {
return b.b.Replay(&replayer{writer: w})
}
// replayer is a small wrapper to implement the correct replay methods.
type replayer struct {
writer ethdb.KeyValueWriter
failure error
}
// Put inserts the given value into the key-value data store.
func (r *replayer) Put(key, value []byte) {
// If the replay already failed, stop executing ops
if r.failure != nil {
return
}
r.failure = r.writer.Put(key, value)
}
// Delete removes the key from the key-value data store.
func (r *replayer) Delete(key []byte) {
// If the replay already failed, stop executing ops
if r.failure != nil {
return
}
r.failure = r.writer.Delete(key)
}
// bytesPrefixRange returns key range that satisfy
// - the given prefix, and
// - the given seek position
func bytesPrefixRange(prefix, start []byte) *util.Range {
r := util.BytesPrefix(prefix)
r.Start = append(r.Start, start...)
return r
}
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package leveldb
import (
"testing"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/ethdb/dbtest"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/storage"
)
func TestLevelDB(t *testing.T) {
t.Run("DatabaseSuite", func(t *testing.T) {
dbtest.TestDatabaseSuite(t, func() ethdb.KeyValueStore {
db, err := leveldb.Open(storage.NewMemStorage(), nil)
if err != nil {
t.Fatal(err)
}
return &Database{
db: db,
}
})
})
}
func BenchmarkLevelDB(b *testing.B) {
dbtest.BenchDatabaseSuite(b, func() ethdb.KeyValueStore {
db, err := leveldb.Open(storage.NewMemStorage(), nil)
if err != nil {
b.Fatal(err)
}
return &Database{
db: db,
}
})
}
# Go orderbook
Improved matching engine written in Go (Golang)
[![Go Report Card](https://goreportcard.com/badge/github.com/i25959341/orderbook)](https://goreportcard.com/report/github.com/i25959341/orderbook)
[![GoDoc](https://godoc.org/github.com/i25959341/orderbook?status.svg)](https://godoc.org/github.com/i25959341/orderbook)
[![gocover.run](https://gocover.run/github.com/i25959341/orderbook.svg?style=flat&tag=1.10)](https://gocover.run?tag=1.10&repo=github.com%2Fi25959341%2Forderbook)
[![Stability: Active](https://masterminds.github.io/stability/active.svg)](https://masterminds.github.io/stability/active.html)
[![Build Status](https://travis-ci.org/i25959341/orderbook.svg?branch=master)](https://travis-ci.org/i25959341/orderbook)
## Features
- Standard price-time priority
- Supports both market and limit orders
- Supports order cancelling
- High performance (above 300k trades per second)
- Optimal memory usage
- JSON Marshalling and Unmarsalling
- Calculating market price for definite quantity
## Usage
To start using order book you need to create object:
```go
import (
"fmt"
ob "github.com/muzykantov/orderbook"
)
func main() {
orderBook := ob.NewOrderBook()
fmt.Println(orderBook)
}
```
Then you be able to use next primary functions:
```go
func (ob *OrderBook) ProcessLimitOrder(side Side, orderID string, quantity, price decimal.Decimal) (done []*Order, partial *Order, err error) { ... }
func (ob *OrderBook) ProcessMarketOrder(side Side, quantity decimal.Decimal) (done []*Order, partial *Order, quantityLeft decimal.Decimal, err error) { .. }
func (ob *OrderBook) CancelOrder(orderID string) *Order { ... }
```
## About primary functions
### ProcessLimitOrder
```go
// ProcessLimitOrder places new order to the OrderBook
// Arguments:
// side - what do you want to do (ob.Sell or ob.Buy)
// orderID - unique order ID in depth
// quantity - how much quantity you want to sell or buy
// price - no more expensive (or cheaper) this price
// * to create new decimal number you should use decimal.New() func
// read more at https://github.com/shopspring/decimal
// Return:
// error - not nil if quantity (or price) is less or equal 0. Or if order with given ID is exists
// done - not nil if your order produces ends of anoter order, this order will add to
// the "done" slice. If your order have done too, it will be places to this array too
// partial - not nil if your order has done but top order is not fully done. Or if your order is
// partial done and placed to the orderbook without full quantity - partial will contain
// your order with quantity to left
// partialQuantityProcessed - if partial order is not nil this result contains processed quatity from partial order
func (ob *OrderBook) ProcessLimitOrder(side Side, orderID string, quantity, price decimal.Decimal) (done []*Order, partial *Order, err error) { ... }
```
For example:
```
ProcessLimitOrder(ob.Sell, "uinqueID", decimal.New(55, 0), decimal.New(100, 0))
asks: 110 -> 5 110 -> 5
100 -> 1 100 -> 56
-------------- -> --------------
bids: 90 -> 5 90 -> 5
80 -> 1 80 -> 1
done - nil
partial - nil
```
```
ProcessLimitOrder(ob.Buy, "uinqueID", decimal.New(7, 0), decimal.New(120, 0))
asks: 110 -> 5
100 -> 1
-------------- -> --------------
bids: 90 -> 5 120 -> 1
80 -> 1 90 -> 5
80 -> 1
done - 2 (or more orders)
partial - uinqueID order
```
```
ProcessLimitOrder(ob.Buy, "uinqueID", decimal.New(3, 0), decimal.New(120, 0))
asks: 110 -> 5
100 -> 1 110 -> 3
-------------- -> --------------
bids: 90 -> 5 90 -> 5
80 -> 1 90 -> 5
done - 1 order with 100 price, (may be also few orders with 110 price) + uinqueID order
partial - 1 order with price 110
```
### ProcessMarketOrder
```go
// ProcessMarketOrder immediately gets definite quantity from the order book with market price
// Arguments:
// side - what do you want to do (ob.Sell or ob.Buy)
// quantity - how much quantity you want to sell or buy
// * to create new decimal number you should use decimal.New() func
// read more at https://github.com/shopspring/decimal
// Return:
// error - not nil if price is less or equal 0
// done - not nil if your market order produces ends of anoter orders, this order will add to
// the "done" slice
// partial - not nil if your order has done but top order is not fully done
// partialQuantityProcessed - if partial order is not nil this result contains processed quatity from partial order
// quantityLeft - more than zero if it is not enought orders to process all quantity
func (ob *OrderBook) ProcessMarketOrder(side Side, quantity decimal.Decimal) (done []*Order, partial *Order, quantityLeft decimal.Decimal, err error) { .. }
```
For example:
```
ProcessMarketOrder(ob.Sell, decimal.New(6, 0))
asks: 110 -> 5 110 -> 5
100 -> 1 100 -> 1
-------------- -> --------------
bids: 90 -> 5 80 -> 1
80 -> 2
done - 2 (or more orders)
partial - 1 order with price 80
quantityLeft - 0
```
```
ProcessMarketOrder(ob.Buy, decimal.New(10, 0))
asks: 110 -> 5
100 -> 1
-------------- -> --------------
bids: 90 -> 5 90 -> 5
80 -> 1 80 -> 1
done - 2 (or more orders)
partial - nil
quantityLeft - 4
```
### CancelOrder
```go
// CancelOrder removes order with given ID from the order book
func (ob *OrderBook) CancelOrder(orderID string) *Order { ... }
```
```
CancelOrder("myUinqueID-Sell-1-with-100")
asks: 110 -> 5
100 -> 1 110 -> 5
-------------- -> --------------
bids: 90 -> 5 90 -> 5
80 -> 1 80 -> 1
done - 2 (or more orders)
partial - nil
quantityLeft - 4
```
## License
The MIT License (MIT)
See LICENSE and AUTHORS files
package orderbook
import "errors"
// OrderBook 错误定义
var (
ErrInvalidQuantity = errors.New("orderbook: 订单数量无效")
ErrInvalidPrice = errors.New("orderbook: 订单价格无效")
ErrOrderExists = errors.New("orderbook: 订单已存在")
ErrOrderNotExists = errors.New("orderbook: 订单不存在")
ErrInsufficientQuantity = errors.New("orderbook: 计算价格的数量不足")
ErrOverflow = errors.New("orderbook: 计算价格的数量溢出")
)
package orderbook
import (
"bytes"
"encoding/gob"
"encoding/json"
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
)
// Order 存储订单请求的信息
type Order struct {
Creator common.Address
Side Side
Id string
Timestamp uint64
Quantity *uint256.Int
Price *uint256.Int
Filled *uint256.Int
AvgPrice *uint256.Int
FillAll bool
}
// MarketView 表示订单簿的快照视图
type MarketView struct {
Asks map[string]*uint256.Int `json:"asks"`
Bids map[string]*uint256.Int `json:"bids"`
}
// NewOrder 创建新的不可变Order对象
func NewOrder(orderID string, creator common.Address, side Side, quantity, price *uint256.Int, timestamp uint64) *Order {
return &Order{
Creator: creator,
Side: side,
Id: orderID,
Quantity: quantity,
Price: price,
Timestamp: timestamp,
Filled: uint256.NewInt(0),
}
}
func (o *Order) Fill(v *uint256.Int) {
o.Filled = new(uint256.Int).Add(o.Filled, v)
if o.Filled.Cmp(o.Quantity) == 0 {
o.FillAll = true
}
}
func (o *Order) IsFilled() bool {
return o.FillAll
}
func (o *Order) Remaining() *uint256.Int {
return new(uint256.Int).Sub(o.Quantity, o.Filled)
}
// String 实现Stringer接口
func (o *Order) String() string {
return fmt.Sprintf("\n\"%s\":\n\t交易方向: %s\n\t数量: %s\n\t价格: %s\n\t时间: %d\n", o.Id, o.Side, o.Quantity, o.Price, o.Timestamp)
}
// MarshalJSON 实现json.Marshaler接口
func (o *Order) MarshalJSON() ([]byte, error) {
return json.Marshal(
&struct {
S Side `json:"side"`
ID string `json:"id"`
Timestamp uint64 `json:"timestamp"`
Quantity *uint256.Int `json:"quantity"`
Price *uint256.Int `json:"price"`
}{
S: o.Side,
ID: o.Id,
Timestamp: o.Timestamp,
Quantity: o.Quantity,
Price: o.Price,
},
)
}
// UnmarshalJSON 实现json.Unmarshaler接口
func (o *Order) UnmarshalJSON(data []byte) error {
obj := struct {
S Side `json:"side"`
ID string `json:"id"`
Timestamp uint64 `json:"timestamp"`
Quantity *uint256.Int `json:"quantity"`
Price *uint256.Int `json:"price"`
}{}
if err := json.Unmarshal(data, &obj); err != nil {
return err
}
o.Side = obj.S
o.Id = obj.ID
o.Timestamp = obj.Timestamp
o.Quantity = obj.Quantity
o.Price = obj.Price
return nil
}
func (o *Order) Dump() ([]byte, error) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
if err := enc.Encode(o); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (o *Order) Load(data []byte) error {
decoder := gob.NewDecoder(bytes.NewBuffer(data))
if err := decoder.Decode(o); err != nil {
return err
}
return nil
}
// GetOrderSide 获取市场一方的订单方向及其订单
func (ob *OrderBook) GetOrderSide(side Side) *OrderSide {
switch side {
case Buy:
return ob.bids
default:
return ob.asks
}
}
// MarketOverview 提供市场概览,包括市场每一方的数量和价格
// asks: qty price bids: qty price
//
// 0.2 14 0.9 13
// 0.1 14.5 5 14
// 0.8 16 2 16
func (ob *OrderBook) MarketOverview() *MarketView {
return &MarketView{
Asks: compileOrders(ob.asks),
Bids: compileOrders(ob.bids),
}
}
// compileOrders 按以下格式编译订单
func compileOrders(orders *OrderSide) map[string]*uint256.Int {
// 显示队列
queue := make(map[string]*uint256.Int)
if orders != nil {
level := orders.MaxPriceQueue()
for level != nil {
if q, exists := queue[level.Price().String()]; exists {
queue[level.Price().String()] = new(uint256.Int).Add(q, level.Volume())
} else {
queue[level.Price().String()] = level.Volume()
}
level = orders.LessThan(level.Price())
}
}
return queue
}
func (ob *OrderBook) Dump() ([]byte, error) {
var buffer bytes.Buffer
encoder := gob.NewEncoder(&buffer)
if err := encoder.Encode(ob); err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func (ob *OrderBook) Load(data []byte) error {
decoder := gob.NewDecoder(bytes.NewBuffer(data))
if err := decoder.Decode(ob); err != nil {
return err
}
return nil
}
package orderbook
import (
"encoding/json"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
)
func TestNewOrder(t *testing.T) {
t.Log(NewOrder("order-1", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(100), uint256.NewInt(100), 1234567890))
}
func TestOrderJSON(t *testing.T) {
data := []*Order{
NewOrder("one", common.HexToAddress("0x0000000000000000000000000000000000000000"), Buy, uint256.NewInt(11), uint256.NewInt(110), 1234567890),
NewOrder("two", common.HexToAddress("0x0000000000000000000000000000000000000000"), Buy, uint256.NewInt(22), uint256.NewInt(220), 1234567890),
NewOrder("three", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(33), uint256.NewInt(330), 1234567890),
NewOrder("four", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(44), uint256.NewInt(440), 1234567890),
}
result, _ := json.Marshal(data)
t.Log(string(result))
data = []*Order{}
_ = json.Unmarshal(result, &data)
t.Log(data)
err := json.Unmarshal([]byte(`[{"side":"fake"}]`), &data)
if err == nil {
t.Fatal("can unmarshal unsupported value")
}
}
package orderbook
import (
"container/list"
"encoding/json"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
)
// OrderBook 实现标准撮合算法
type OrderBook struct {
orders map[string]*list.Element // orderID -> *Order (*list.Element.Value.(*Order))
asks *OrderSide
bids *OrderSide
BaseToken string
QuoteToken string
}
// NewOrderBook 创建订单簿对象
// NewOrderBook 创建并返回一个新的OrderBook实例,包含买(bids)和卖(asks)订单侧
// 以及一个用于存储订单的map。
func NewOrderBook(baseToken, quoteToken string) *OrderBook {
return &OrderBook{
orders: map[string]*list.Element{},
bids: NewOrderSide(),
asks: NewOrderSide(),
BaseToken: baseToken,
QuoteToken: quoteToken,
}
}
// PriceLevel 包含深度中的价格和数量信息
type PriceLevel struct {
Price *uint256.Int `json:"price"`
Quantity *uint256.Int `json:"quantity"`
}
// ProcessMarketOrder 以市价立即从订单簿中获取指定数量
// 参数:
//
// side - 交易方向 (ob.Sell 或 ob.Buy)
// quantity - 想要买入或卖出的数量
// * 使用 uint256.NewInt() 函数创建新的 uint256 数字
// 更多信息请参考 https://github.com/holiman/uint256
//
// 返回:
//
// error - 如果价格小于等于0则返回非nil
// done - 如果您的市价单导致其他订单完成,这些订单将被添加到"done"切片中
// partial - 如果您的订单已完成但顶部订单未完全完成,则为非nil
// partialQuantityProcessed - 如果partial订单非nil,此结果包含从partial订单处理的数量
// quantityLeft - 如果没有足够的订单处理所有数量,则大于零
func (ob *OrderBook) CalculatePriceAfterExecution(side Side, orderId string, quantity *uint256.Int) (price *uint256.Int, err error) {
price = uint256.NewInt(0)
var (
level *OrderQueue
iter func(*uint256.Int) *OrderQueue
)
if side == Buy {
level = ob.asks.MinPriceQueue()
iter = ob.asks.GreaterThan
} else {
level = ob.bids.MaxPriceQueue()
iter = ob.bids.LessThan
}
for quantity.Cmp(uint256.NewInt(0)) > 0 && level != nil {
levelVolume := level.Volume()
levelPrice := level.Price()
if quantity.Cmp(levelVolume) >= 0 {
price = levelPrice
overflow := false
quantity, overflow = new(uint256.Int).SubOverflow(quantity, levelVolume)
if overflow {
return nil, ErrOverflow
}
level = iter(levelPrice)
} else {
price = levelPrice
quantity = uint256.NewInt(0)
}
}
return
}
// ProcessMarketOrder 以市价立即从订单簿中获取指定数量
// 参数:
//
// side - 交易方向 (ob.Sell 或 ob.Buy)
// quantity - 想要买入或卖出的数量
// * 使用 uint256.NewInt() 函数创建新的 uint256 数字
// 更多信息请参考 https://github.com/holiman/uint256
//
// 返回:
//
// error - 如果价格小于等于0则返回非nil
// done - 如果您的市价单导致其他订单完成,这些订单将被添加到"done"切片中
// partial - 如果您的订单已完成但顶部订单未完全完成,则为非nil
// partialQuantityProcessed - 如果partial订单非nil,此结果包含从partial订单处理的数量
// quantityLeft - 如果没有足够的订单处理所有数量,则大于零
func (ob *OrderBook) ProcessMarketOrder(side Side, quantity *uint256.Int) (done []*Order, partial *Order, partialQuantityProcessed, quantityLeft *uint256.Int, err error) {
if quantity.Cmp(uint256.NewInt(0)) <= 0 {
return nil, nil, uint256.NewInt(0), uint256.NewInt(0), ErrInvalidQuantity
}
var (
iter func() *OrderQueue
sideToProcess *OrderSide
)
if side == Buy {
iter = ob.asks.MinPriceQueue
sideToProcess = ob.asks
} else {
iter = ob.bids.MaxPriceQueue
sideToProcess = ob.bids
}
for quantity.Cmp(uint256.NewInt(0)) > 0 && sideToProcess.Len() > 0 {
bestPrice := iter()
ordersDone, partialDone, partialQty, quantityLeft, err := ob.processQueue(bestPrice, quantity)
if err != nil {
return nil, nil, uint256.NewInt(0), uint256.NewInt(0), err
}
done = append(done, ordersDone...)
partial = partialDone
partialQuantityProcessed = partialQty
quantity = quantityLeft
}
quantityLeft = quantity
return
}
// ProcessLimitOrder 将新订单放入订单簿
// 参数:
//
// side - 交易方向 (ob.Sell 或 ob.Buy)
// orderID - 深度中的唯一订单ID
// creator - 订单创建者地址
// quantity - 想要买入或卖出的数量
// price - 不高于(或低于)此价格
// * 使用 uint256.NewInt() 函数创建新的 uint256 数字
// 更多信息请参考 https://github.com/holiman/uint256
//
// 返回:
//
// error - 如果数量(或价格)小于等于0,或给定ID的订单已存在,则返回非nil
// done - 如果您的订单导致其他订单完成,这些订单将被添加到"done"切片中
// 如果您的订单也完成了,它也会被放入此数组
// partial - 如果您的订单已完成但顶部订单未完全完成,或如果您的订单部分完成
// 并且剩余数量被放入订单簿 - partial将包含您剩余数量的订单
// partialQuantityProcessed - 如果partial订单非nil,此结果包含从partial订单处理的数量
func (ob *OrderBook) ProcessLimitOrder(side Side, orderID string, creator common.Address, quantity, price *uint256.Int, nonce uint64) (done []*Order, partial *Order, makerOrder *Order, partialQuantityProcessed, quantityLeft *uint256.Int, err error) {
if _, ok := ob.orders[orderID]; ok {
return nil, nil, nil, uint256.NewInt(0), uint256.NewInt(0), ErrOrderExists
}
if quantity.Cmp(uint256.NewInt(0)) <= 0 {
return nil, nil, nil, uint256.NewInt(0), uint256.NewInt(0), ErrInvalidQuantity
}
if price.Cmp(uint256.NewInt(0)) <= 0 {
return nil, nil, nil, uint256.NewInt(0), uint256.NewInt(0), ErrInvalidPrice
}
partialQuantityProcessed = uint256.NewInt(0)
quantityToTrade := quantity
quantityLeft = quantity
var (
sideToProcess *OrderSide
sideToAdd *OrderSide
comparator func(*uint256.Int) bool
iter func() *OrderQueue
overflow bool
)
if side == Buy {
sideToAdd = ob.bids
sideToProcess = ob.asks
comparator = func(p *uint256.Int) bool { return price.Cmp(p) >= 0 }
iter = ob.asks.MinPriceQueue
} else {
sideToAdd = ob.asks
sideToProcess = ob.bids
comparator = func(p *uint256.Int) bool { return price.Cmp(p) <= 0 }
iter = ob.bids.MaxPriceQueue
}
bestPrice := iter()
for quantityToTrade.Cmp(uint256.NewInt(0)) > 0 && sideToProcess.Len() > 0 && comparator(bestPrice.Price()) {
ordersDone, partialDone, partialQty, quantityLeft, error := ob.processQueue(bestPrice, quantityToTrade)
if error != nil {
return nil, nil, nil, nil, nil, error
}
done = append(done, ordersDone...)
partial = partialDone
partialQuantityProcessed = partialQty
quantityToTrade = quantityLeft
bestPrice = iter()
}
if quantityToTrade.Cmp(uint256.NewInt(0)) > 0 {
o := NewOrder(orderID, creator, side, quantityToTrade, price, nonce)
if len(done) > 0 {
partialQuantityProcessed, overflow = new(uint256.Int).SubOverflow(quantity, quantityToTrade)
if overflow {
return nil, nil, nil, nil, nil, ErrOverflow
}
partial = o
}
makerOrder = o
ob.orders[orderID], err = sideToAdd.Append(o)
if err != nil {
return nil, nil, nil, nil, nil, err
}
} else {
totalQuantity := uint256.NewInt(0)
totalPrice := uint256.NewInt(0)
for _, order := range done {
totalQuantity, overflow = new(uint256.Int).AddOverflow(totalQuantity, order.Quantity)
if overflow {
return nil, nil, nil, nil, nil, ErrOverflow
}
price, overflow := new(uint256.Int).MulOverflow(order.Price, order.Quantity)
if overflow {
return nil, nil, nil, nil, nil, ErrOverflow
}
totalPrice, overflow = new(uint256.Int).AddOverflow(totalPrice, price)
if overflow {
return nil, nil, nil, nil, nil, ErrOverflow
}
}
if partialQuantityProcessed.Cmp(uint256.NewInt(0)) > 0 {
totalQuantity, overflow = new(uint256.Int).AddOverflow(totalQuantity, partialQuantityProcessed)
if overflow {
return nil, nil, nil, nil, nil, ErrOverflow
}
price, overflow := new(uint256.Int).MulOverflow(partial.Price, partialQuantityProcessed)
if overflow {
return nil, nil, nil, nil, nil, ErrOverflow
}
totalPrice, overflow = new(uint256.Int).AddOverflow(totalPrice, price)
if overflow {
return nil, nil, nil, nil, nil, ErrOverflow
}
}
orderPrice := new(uint256.Int).Div(totalPrice, totalQuantity)
done = append(done, NewOrder(orderID, creator, side, quantity, orderPrice, nonce))
}
return
}
// partial.quantity = 是更新过的对手的订单
// partialQuantityProcessed = 最后一单成交了多少
func (ob *OrderBook) processQueue(orderQueue *OrderQueue, quantityToTrade *uint256.Int) (done []*Order, partial *Order, partialQuantityProcessed, quantityLeft *uint256.Int, err error) {
partialQuantityProcessed = uint256.NewInt(0)
quantityLeft = quantityToTrade
for orderQueue.Len() > 0 && quantityLeft.Cmp(uint256.NewInt(0)) > 0 {
headOrderEl := orderQueue.Head()
headOrder := headOrderEl.Value.(*Order)
if quantityLeft.Cmp(headOrder.Remaining()) < 0 {
quantity, overflow := new(uint256.Int).SubOverflow(headOrder.Remaining(), quantityLeft)
if overflow {
return nil, nil, uint256.NewInt(0), uint256.NewInt(0), ErrOverflow
}
headOrder.Fill(quantity)
partial = headOrder
partialQuantityProcessed = quantityLeft
orderQueue.Update(headOrderEl, partial)
quantityLeft = uint256.NewInt(0)
} else {
overflow := false
quantityLeft, overflow = new(uint256.Int).SubOverflow(quantityLeft, headOrder.Remaining())
if overflow {
return nil, nil, uint256.NewInt(0), uint256.NewInt(0), ErrOverflow
}
order, err := ob.CancelOrder(headOrder.Id)
if err != nil {
return nil, nil, uint256.NewInt(0), uint256.NewInt(0), err
}
order.Fill(headOrder.Remaining())
done = append(done, order)
}
}
return
}
// Order 通过ID返回订单
func (ob *OrderBook) Order(orderID string) *Order {
e, ok := ob.orders[orderID]
if !ok {
return nil
}
return e.Value.(*Order)
}
// Depth 返回价格层级和每个价格层级的数量
func (ob *OrderBook) Depth() (asks, bids []*PriceLevel) {
level := ob.asks.MaxPriceQueue()
for level != nil {
asks = append(asks, &PriceLevel{
Price: level.Price(),
Quantity: level.Volume(),
})
level = ob.asks.LessThan(level.Price())
}
level = ob.bids.MaxPriceQueue()
for level != nil {
bids = append(bids, &PriceLevel{
Price: level.Price(),
Quantity: level.Volume(),
})
level = ob.bids.LessThan(level.Price())
}
return
}
// CancelOrder 从订单簿中删除给定ID的订单
func (ob *OrderBook) CancelOrder(orderID string) (*Order, error) {
e, ok := ob.orders[orderID]
if !ok {
return nil, nil
}
delete(ob.orders, orderID)
if e.Value.(*Order).Side == Buy {
return ob.bids.Remove(e)
}
return ob.asks.Remove(e)
}
// CalculateMarketPrice 返回请求数量的总市场价格
// 如果err不为nil,price返回该方向所有层级的总价格
func (ob *OrderBook) CalculateMarketPrice(side Side, quantity *uint256.Int) (price *uint256.Int, quant *uint256.Int, err error) {
price = uint256.NewInt(0)
quant = uint256.NewInt(0)
var (
level *OrderQueue
iter func(*uint256.Int) *OrderQueue
)
if side == Buy {
level = ob.asks.MinPriceQueue()
iter = ob.asks.GreaterThan
} else {
level = ob.bids.MaxPriceQueue()
iter = ob.bids.LessThan
}
for quantity.Cmp(uint256.NewInt(0)) > 0 && level != nil {
levelVolume := level.Volume()
levelPrice := level.Price()
if quantity.Cmp(levelVolume) >= 0 {
overflow := false
_price, overflow := new(uint256.Int).MulOverflow(levelPrice, levelVolume)
if overflow {
return nil, nil, ErrOverflow
}
price, overflow = new(uint256.Int).AddOverflow(price, _price)
if overflow {
return nil, nil, ErrOverflow
}
quantity, overflow = new(uint256.Int).SubOverflow(quantity, levelVolume)
if overflow {
return nil, nil, ErrOverflow
}
quant, overflow = new(uint256.Int).AddOverflow(quant, levelVolume)
if overflow {
return nil, nil, ErrOverflow
}
level = iter(levelPrice)
} else {
overflow := false
_price, overflow := new(uint256.Int).MulOverflow(levelPrice, quantity)
if overflow {
return nil, nil, ErrOverflow
}
price, overflow = new(uint256.Int).AddOverflow(price, _price)
if overflow {
return nil, nil, ErrOverflow
}
quant, overflow = new(uint256.Int).AddOverflow(quant, quantity)
if overflow {
return nil, nil, ErrOverflow
}
quantity = uint256.NewInt(0)
}
}
if quantity.Cmp(uint256.NewInt(0)) > 0 {
err = ErrInsufficientQuantity
}
return
}
// String 实现fmt.Stringer接口
func (ob *OrderBook) String() string {
return ob.asks.String() + "\r\n------------------------------------" + ob.bids.String()
}
// MarshalJSON 实现json.Marshaler接口
func (ob *OrderBook) MarshalJSON() ([]byte, error) {
return json.Marshal(
&struct {
Asks *OrderSide `json:"asks"`
Bids *OrderSide `json:"bids"`
}{
Asks: ob.asks,
Bids: ob.bids,
},
)
}
// UnmarshalJSON 实现json.Unmarshaler接口
func (ob *OrderBook) UnmarshalJSON(data []byte) error {
obj := struct {
Asks *OrderSide `json:"asks"`
Bids *OrderSide `json:"bids"`
}{}
if err := json.Unmarshal(data, &obj); err != nil {
return err
}
ob.asks = obj.Asks
ob.bids = obj.Bids
ob.orders = map[string]*list.Element{}
for _, order := range ob.asks.Orders() {
ob.orders[order.Value.(*Order).Id] = order
}
for _, order := range ob.bids.Orders() {
ob.orders[order.Value.(*Order).Id] = order
}
return nil
}
package orderbook
import (
"fmt"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
)
func addDepth(ob *OrderBook, prefix string, quantity *uint256.Int) {
creator := common.Address{}
for i := 50; i < 100; i = i + 10 {
ob.ProcessLimitOrder(Buy, fmt.Sprintf("%sbuy-%d", prefix, i), creator, quantity, uint256.NewInt(uint64(i)), 1234567890)
}
for i := 100; i < 150; i = i + 10 {
ob.ProcessLimitOrder(Sell, fmt.Sprintf("%ssell-%d", prefix, i), creator, quantity, uint256.NewInt(uint64(i)), 1234567890)
}
}
func TestLimitPlace(t *testing.T) {
ob := NewOrderBook("USDT", "BTC")
creator := common.Address{}
quantity := uint256.NewInt(2)
for i := 50; i < 100; i = i + 10 {
done, partial, _, partialQty, _, err := ob.ProcessLimitOrder(Buy, fmt.Sprintf("buy-%d", i), creator, quantity, uint256.NewInt(uint64(i)), 1234567890)
if len(done) != 0 {
t.Fatal("OrderBook failed to process limit order (done is not empty)")
}
if partial != nil {
t.Fatal("OrderBook failed to process limit order (partial is not empty)")
}
if partialQty.Sign() != 0 {
t.Fatal("OrderBook failed to process limit order (partialQty is not zero)")
}
if err != nil {
t.Fatal(err)
}
}
for i := 100; i < 150; i = i + 10 {
done, partial, _, partialQty, _, err := ob.ProcessLimitOrder(Sell, fmt.Sprintf("sell-%d", i), creator, quantity, uint256.NewInt(uint64(i)), 1234567890)
if len(done) != 0 {
t.Fatal("OrderBook failed to process limit order (done is not empty)")
}
if partial != nil {
t.Fatal("OrderBook failed to process limit order (partial is not empty)")
}
if partialQty.Sign() != 0 {
t.Fatal("OrderBook failed to process limit order (partialQty is not zero)")
}
if err != nil {
t.Fatal(err)
}
}
t.Log(ob)
if ob.Order("fake") != nil {
t.Fatal("can get fake order")
}
if ob.Order("sell-100") == nil {
t.Fatal("can't get real order")
}
t.Log(ob.Depth())
return
}
func TestLimitProcess(t *testing.T) {
ob := NewOrderBook("USDT", "BTC")
addDepth(ob, "", uint256.NewInt(2))
creator := common.Address{}
done, partial, _, partialQty, _, err := ob.ProcessLimitOrder(Buy, "order-b100", creator, uint256.NewInt(1), uint256.NewInt(100), 1234567890)
if err != nil {
t.Fatal(err)
}
t.Log("Done:", done)
if done[0].Id != "order-b100" {
t.Fatal("Wrong done id")
}
t.Log("Partial:", partial)
if partial.Id != "sell-100" {
t.Fatal("Wrong partial id")
}
if partialQty.Cmp(uint256.NewInt(1)) != 0 {
t.Fatal("Wrong partial quantity processed")
}
t.Log(ob)
done, partial, _, partialQty, _, err = ob.ProcessLimitOrder(Buy, "order-b150", creator, uint256.NewInt(10), uint256.NewInt(150), 1234567890)
if err != nil {
t.Fatal(err)
}
t.Log("Done:", done)
if len(done) != 5 {
t.Fatal("Wrong done quantity")
}
t.Log("Partial:", partial)
if partial.Id != "order-b150" {
t.Fatal("Wrong partial id")
}
if partialQty.Cmp(uint256.NewInt(9)) != 0 {
t.Fatal("Wrong partial quantity processed", partialQty)
}
t.Log(ob)
if _, _, _, _, _, err := ob.ProcessLimitOrder(Sell, "buy-70", creator, uint256.NewInt(11), uint256.NewInt(40), 1234567890); err == nil {
t.Fatal("Can add existing order")
}
if _, _, _, _, _, err := ob.ProcessLimitOrder(Sell, "fake-70", creator, uint256.NewInt(0), uint256.NewInt(40), 1234567890); err == nil {
t.Fatal("Can add empty quantity order")
}
if _, _, _, _, _, err := ob.ProcessLimitOrder(Sell, "fake-70", creator, uint256.NewInt(10), uint256.NewInt(0), 1234567890); err == nil {
t.Fatal("Can add zero price")
}
if o, err := ob.CancelOrder("order-b100"); o != nil || err != nil {
t.Fatal("Can cancel done order")
}
done, partial, _, partialQty, _, err = ob.ProcessLimitOrder(Sell, "order-s40", creator, uint256.NewInt(11), uint256.NewInt(40), 1234567890)
if err != nil {
t.Fatal(err)
}
t.Log("Done:", done)
if len(done) != 7 {
t.Fatal("Wrong done quantity")
}
if partial != nil {
t.Fatal("Wrong partial")
}
if partialQty.Sign() != 0 {
t.Fatal("Wrong partialQty")
}
t.Log(ob)
}
func TestMarketProcess(t *testing.T) {
ob := NewOrderBook("USDT", "BTC")
addDepth(ob, "", uint256.NewInt(2))
done, partial, partialQty, left, err := ob.ProcessMarketOrder(Buy, uint256.NewInt(3))
if err != nil {
t.Fatal(err)
}
if left.Sign() > 0 {
t.Fatal("Wrong quantity left")
}
if partialQty.Cmp(uint256.NewInt(1)) != 0 {
t.Fatal("Wrong partial quantity left")
}
t.Log("Done", done)
t.Log("Partial", partial)
t.Log(ob)
if _, _, _, _, err := ob.ProcessMarketOrder(Buy, uint256.NewInt(0)); err == nil {
t.Fatal("Can add zero quantity order")
}
done, partial, partialQty, left, err = ob.ProcessMarketOrder(Sell, uint256.NewInt(12))
if err != nil {
t.Fatal(err)
}
if partial != nil {
t.Fatal("Partial is not nil")
}
if partialQty.Sign() != 0 {
t.Fatal("PartialQty is not nil")
}
if len(done) != 5 {
t.Fatal("Invalid done amount")
}
if left.Cmp(uint256.NewInt(2)) != 0 {
t.Fatal("Invalid left amount", left)
}
t.Log("Done", done)
t.Log(ob)
}
// func TestOrderBookJSON(t *testing.T) {
// data := NewOrderBook("USDT", "BTC")
// result, _ := json.Marshal(data)
// t.Log(string(result))
// if err := json.Unmarshal(result, data); err != nil {
// t.Fatal(err)
// }
// addDepth(data, "01-", uint256.NewInt(10))
// addDepth(data, "02-", uint256.NewInt(1))
// addDepth(data, "03-", uint256.NewInt(2))
// result, _ = json.Marshal(data)
// t.Log(string(result))
// data = NewOrderBook("USDT", "BTC")
// if err := json.Unmarshal(result, data); err != nil {
// t.Fatal(err)
// }
// t.Log(data)
// err := json.Unmarshal([]byte(`[{"side":"fake"}]`), &data)
// if err == nil {
// t.Fatal("can unmarshal unsupported value")
// }
// }
func TestPriceCalculation(t *testing.T) {
ob := NewOrderBook("USDT", "BTC")
addDepth(ob, "05-", uint256.NewInt(10))
addDepth(ob, "10-", uint256.NewInt(10))
addDepth(ob, "15-", uint256.NewInt(10))
t.Log(ob)
price, _, err := ob.CalculateMarketPrice(Buy, uint256.NewInt(115))
if err != nil {
t.Fatal(err)
}
if price.Cmp(uint256.NewInt(13150)) != 0 {
t.Fatal("invalid price", price)
}
price, _, err = ob.CalculateMarketPrice(Buy, uint256.NewInt(200))
if err == nil {
t.Fatal("invalid quantity count")
}
if price.Cmp(uint256.NewInt(18000)) != 0 {
t.Fatal("invalid price", price)
}
// -------
price, _, err = ob.CalculateMarketPrice(Sell, uint256.NewInt(115))
if err != nil {
t.Fatal(err)
}
if price.Cmp(uint256.NewInt(8700)) != 0 {
t.Fatal("invalid price", price)
}
price, _, err = ob.CalculateMarketPrice(Sell, uint256.NewInt(200))
if err == nil {
t.Fatal("invalid quantity count")
}
if price.Cmp(uint256.NewInt(10500)) != 0 {
t.Fatal("invalid price", price)
}
}
func BenchmarkLimitOrder(b *testing.B) {
ob := NewOrderBook("USDT", "BTC")
stopwatch := time.Now()
creator := common.Address{}
for i := 0; i < b.N; i++ {
addDepth(ob, "05-", uint256.NewInt(10)) // 10 ts
addDepth(ob, "10-", uint256.NewInt(10)) // 10 ts
addDepth(ob, "15-", uint256.NewInt(10)) // 10 ts
ob.ProcessLimitOrder(Buy, "order-b150", creator, uint256.NewInt(160), uint256.NewInt(150), 1234567890) // 1 ts
ob.ProcessMarketOrder(Sell, uint256.NewInt(200)) // 1 ts = total 32
}
elapsed := time.Since(stopwatch)
fmt.Printf("\n\nElapsed: %s\nTransactions per second (avg): %f\n", elapsed, float64(b.N*32)/elapsed.Seconds())
}
package orderbook
import (
"container/list"
"encoding/json"
"fmt"
"strings"
"github.com/holiman/uint256"
)
// OrderQueue 存储和管理订单链
type OrderQueue struct {
volume *uint256.Int
price *uint256.Int
orders *list.List
}
// NewOrderQueue 创建并初始化OrderQueue对象
func NewOrderQueue(price *uint256.Int) *OrderQueue {
return &OrderQueue{
price: price,
volume: uint256.NewInt(0),
orders: list.New(),
}
}
// Len 返回队列中的订单数量
func (oq *OrderQueue) Len() int {
return oq.orders.Len()
}
// Price 返回队列的价格等级
func (oq *OrderQueue) Price() *uint256.Int {
return oq.price
}
// Volume 返回总订单量
func (oq *OrderQueue) Volume() *uint256.Int {
return oq.volume
}
// Head 返回队列中的顶部订单
func (oq *OrderQueue) Head() *list.Element {
return oq.orders.Front()
}
// Tail 返回队列中的底部订单
func (oq *OrderQueue) Tail() *list.Element {
return oq.orders.Back()
}
// Append 将订单添加到队列尾部
func (oq *OrderQueue) Append(o *Order) (*list.Element, error) {
overflow := false
oq.volume, overflow = new(uint256.Int).AddOverflow(oq.volume, o.Quantity)
if overflow {
return nil, ErrOverflow
}
return oq.orders.PushBack(o), nil
}
// Update 在列表值中设置新订单
func (oq *OrderQueue) Update(e *list.Element, o *Order) (*list.Element, error) {
overflow := false
oq.volume, overflow = new(uint256.Int).SubOverflow(oq.volume, e.Value.(*Order).Quantity)
if overflow {
return nil, ErrOverflow
}
oq.volume, overflow = new(uint256.Int).AddOverflow(oq.volume, o.Quantity)
if overflow {
return nil, ErrOverflow
}
e.Value = o
return e, nil
}
// Remove 从队列中移除订单并链接订单链
func (oq *OrderQueue) Remove(e *list.Element) (*Order, error) {
overflow := false
oq.volume, overflow = new(uint256.Int).SubOverflow(oq.volume, e.Value.(*Order).Quantity)
if overflow {
return nil, ErrOverflow
}
return oq.orders.Remove(e).(*Order), nil
}
// String 实现fmt.Stringer接口
func (oq *OrderQueue) String() string {
sb := strings.Builder{}
iter := oq.orders.Front()
sb.WriteString(fmt.Sprintf("\n队列长度: %d, 价格: %s, 数量: %s, 订单:", oq.Len(), oq.Price(), oq.Volume()))
for iter != nil {
order := iter.Value.(*Order)
str := fmt.Sprintf("\n\t订单ID: %s, 数量: %s, 价格:%s, 时间: %d", order.Id, order.Quantity, order.Price, order.Timestamp)
sb.WriteString(str)
iter = iter.Next()
}
return sb.String()
}
// MarshalJSON 实现json.Marshaler接口
func (oq *OrderQueue) MarshalJSON() ([]byte, error) {
iter := oq.Head()
var orders []*Order
for iter != nil {
orders = append(orders, iter.Value.(*Order))
iter = iter.Next()
}
return json.Marshal(
&struct {
Volume *uint256.Int `json:"volume"`
Price *uint256.Int `json:"price"`
Orders []*Order `json:"orders"`
}{
Volume: oq.Volume(),
Price: oq.Price(),
Orders: orders,
},
)
}
// UnmarshalJSON 实现json.Unmarshaler接口
func (oq *OrderQueue) UnmarshalJSON(data []byte) error {
obj := struct {
Volume *uint256.Int `json:"volume"`
Price *uint256.Int `json:"price"`
Orders []*Order `json:"orders"`
}{}
if err := json.Unmarshal(data, &obj); err != nil {
return err
}
oq.volume = obj.Volume
oq.price = obj.Price
oq.orders = list.New()
for _, order := range obj.Orders {
oq.orders.PushBack(order)
}
return nil
}
package orderbook
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
)
func TestOrderQueue(t *testing.T) {
price := uint256.NewInt(100)
oq := NewOrderQueue(price)
o1 := NewOrder(
"order-1",
common.HexToAddress("0x0000000000000000000000000000000000000000"),
Buy,
uint256.NewInt(100),
uint256.NewInt(100),
1234567890,
)
o2 := NewOrder(
"order-2",
common.HexToAddress("0x0000000000000000000000000000000000000000"),
Buy,
uint256.NewInt(100),
uint256.NewInt(100),
1234567890,
)
head, _ := oq.Append(o1)
tail, _ := oq.Append(o2)
if head == nil || tail == nil {
t.Fatal("Could not append order to the OrderQueue")
}
if oq.Volume().Cmp(uint256.NewInt(200)) != 0 {
t.Fatalf("Invalid order volume (have: %s, want: 200", oq.Volume())
}
if head.Value.(*Order) != o1 || tail.Value.(*Order) != o2 {
t.Fatal("Invalid element value")
}
if oq.Head() != head || oq.Tail() != tail {
t.Fatal("Invalid element position")
}
if oq.Head().Next() != oq.Tail() || oq.Tail().Prev() != head ||
oq.Head().Prev() != nil || oq.Tail().Next() != nil {
t.Fatal("Invalid element link")
}
o1 = NewOrder(
"order-3",
common.HexToAddress("0x0000000000000000000000000000000000000000"),
Buy,
uint256.NewInt(200),
uint256.NewInt(200),
1234567890,
)
oq.Update(head, o1)
if oq.Volume().Cmp(uint256.NewInt(300)) != 0 {
t.Fatalf("Invalid order volume (have: %s, want: 300", oq.Volume())
}
if o, err := oq.Remove(head); o != o1 || err != nil {
t.Fatal("Invalid element value")
}
if oq.Volume().Cmp(uint256.NewInt(100)) != 0 {
t.Fatalf("Invalid order volume (have: %s, want: 100", oq.Volume())
}
t.Log(oq)
}
func TestOrderQueueJSON(t *testing.T) {
data := NewOrderQueue(uint256.NewInt(111))
data.Append(NewOrder("one", common.HexToAddress("0x0000000000000000000000000000000000000000"), Buy, uint256.NewInt(11), uint256.NewInt(110), 1234567890))
data.Append(NewOrder("two", common.HexToAddress("0x0000000000000000000000000000000000000000"), Buy, uint256.NewInt(22), uint256.NewInt(220), 1234567890))
data.Append(NewOrder("three", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(33), uint256.NewInt(330), 1234567890))
data.Append(NewOrder("four", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(44), uint256.NewInt(440), 1234567890))
result, _ := json.Marshal(data)
t.Log(string(result))
data = NewOrderQueue(uint256.NewInt(0))
if err := json.Unmarshal(result, data); err != nil {
t.Fatal(err)
}
t.Log(data)
err := json.Unmarshal([]byte(`[{"side":"fake"}]`), &data)
if err == nil {
t.Fatal("can unmarshal unsupported value")
}
}
func BenchmarkOrderQueue(b *testing.B) {
price := uint256.NewInt(100)
orderQueue := NewOrderQueue(price)
stopwatch := time.Now()
var o *Order
for i := 0; i < b.N; i++ {
o = NewOrder(
fmt.Sprintf("order-%d", i),
common.HexToAddress("0x0000000000000000000000000000000000000000"),
Buy,
uint256.NewInt(100),
uint256.NewInt(uint64(i)),
uint64(stopwatch.UnixMilli()),
)
orderQueue.Append(o)
}
elapsed := time.Since(stopwatch)
fmt.Printf("\n\nElapsed: %s\nTransactions per second: %f\n", elapsed, float64(b.N)/elapsed.Seconds())
}
package orderbook
import (
"container/list"
"encoding/json"
"fmt"
"strings"
rbtx "github.com/emirpasic/gods/examples/redblacktreeextended"
rbt "github.com/emirpasic/gods/trees/redblacktree"
"github.com/holiman/uint256"
)
// OrderSide 实现订单队列操作的外观模式
type OrderSide struct {
priceTree *rbtx.RedBlackTreeExtended
prices map[string]*OrderQueue
volume *uint256.Int
numOrders int
depth int
}
func rbtComparator(a, b interface{}) int {
return a.(*uint256.Int).Cmp(b.(*uint256.Int))
}
// NewOrderSide 创建新的OrderSide管理器
func NewOrderSide() *OrderSide {
return &OrderSide{
priceTree: &rbtx.RedBlackTreeExtended{
Tree: rbt.NewWith(rbtComparator),
},
prices: map[string]*OrderQueue{},
volume: uint256.NewInt(0),
}
}
// Len 返回订单数量
func (os *OrderSide) Len() int {
return os.numOrders
}
// Depth 返回市场深度
func (os *OrderSide) Depth() int {
return os.depth
}
// Volume 返回该方向的总数量
func (os *OrderSide) Volume() *uint256.Int {
return os.volume
}
// Append 将订单添加到指定价格等级
func (os *OrderSide) Append(o *Order) (*list.Element, error) {
price := o.Price
strPrice := price.String()
priceQueue, ok := os.prices[strPrice]
if !ok {
priceQueue = NewOrderQueue(price)
os.prices[strPrice] = priceQueue
os.priceTree.Put(price, priceQueue)
os.depth++
}
os.numOrders++
overflow := false
fmt.Println(os.volume, o.Quantity)
os.volume, overflow = new(uint256.Int).AddOverflow(os.volume, o.Quantity)
if overflow {
return nil, ErrOverflow
}
return priceQueue.Append(o)
}
// Remove 从指定价格等级移除订单
func (os *OrderSide) Remove(e *list.Element) (*Order, error) {
price := e.Value.(*Order).Price
strPrice := price.String()
priceQueue := os.prices[strPrice]
o, err := priceQueue.Remove(e)
if err != nil {
return nil, err
}
if priceQueue.Len() == 0 {
delete(os.prices, strPrice)
os.priceTree.Remove(price)
os.depth--
}
os.numOrders--
overflow := false
os.volume, overflow = new(uint256.Int).SubOverflow(os.volume, o.Quantity)
if overflow {
return nil, ErrOverflow
}
return o, nil
}
// MaxPriceQueue 返回最高价格等级
func (os *OrderSide) MaxPriceQueue() *OrderQueue {
if os.depth > 0 {
if value, found := os.priceTree.GetMax(); found {
return value.(*OrderQueue)
}
}
return nil
}
// MinPriceQueue 返回最低价格等级
func (os *OrderSide) MinPriceQueue() *OrderQueue {
if os.depth > 0 {
if value, found := os.priceTree.GetMin(); found {
return value.(*OrderQueue)
}
}
return nil
}
// LessThan 返回价格小于给定价格的最近OrderQueue
func (os *OrderSide) LessThan(price *uint256.Int) *OrderQueue {
tree := os.priceTree.Tree
node := tree.Root
var floor *rbt.Node
for node != nil {
if tree.Comparator(price, node.Key) > 0 {
floor = node
node = node.Right
} else {
node = node.Left
}
}
if floor != nil {
return floor.Value.(*OrderQueue)
}
return nil
}
// GreaterThan 返回价格大于给定价格的最近OrderQueue
func (os *OrderSide) GreaterThan(price *uint256.Int) *OrderQueue {
tree := os.priceTree.Tree
node := tree.Root
var ceiling *rbt.Node
for node != nil {
if tree.Comparator(price, node.Key) < 0 {
ceiling = node
node = node.Left
} else {
node = node.Right
}
}
if ceiling != nil {
return ceiling.Value.(*OrderQueue)
}
return nil
}
// Orders 返回所有*list.Element订单
func (os *OrderSide) Orders() (orders []*list.Element) {
for _, price := range os.prices {
iter := price.Head()
for iter != nil {
orders = append(orders, iter)
iter = iter.Next()
}
}
return
}
// String 实现fmt.Stringer接口
func (os *OrderSide) String() string {
sb := strings.Builder{}
level := os.MaxPriceQueue()
for level != nil {
sb.WriteString(fmt.Sprintf("\n%s -> %s", level.Price(), level.Volume()))
level = os.LessThan(level.Price())
}
return sb.String()
}
// MarshalJSON 实现json.Marshaler接口
func (os *OrderSide) MarshalJSON() ([]byte, error) {
return json.Marshal(
&struct {
NumOrders int `json:"numOrders"`
Depth int `json:"depth"`
Prices map[string]*OrderQueue `json:"prices"`
}{
NumOrders: os.numOrders,
Depth: os.depth,
Prices: os.prices,
},
)
}
// UnmarshalJSON 实现json.Unmarshaler接口
func (os *OrderSide) UnmarshalJSON(data []byte) error {
obj := struct {
NumOrders int `json:"numOrders"`
Depth int `json:"depth"`
Prices map[string]*OrderQueue `json:"prices"`
}{}
if err := json.Unmarshal(data, &obj); err != nil {
return err
}
os.numOrders = obj.NumOrders
os.depth = obj.Depth
os.prices = obj.Prices
os.priceTree = &rbtx.RedBlackTreeExtended{
Tree: rbt.NewWith(rbtComparator),
}
for price, queue := range os.prices {
k, err := uint256.FromDecimal(price)
if err != nil {
return err
}
os.priceTree.Put(k, queue)
}
return nil
}
package orderbook
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
)
func TestOrderSide(t *testing.T) {
ot := NewOrderSide()
o1 := NewOrder(
"order-1",
common.HexToAddress("0x0000000000000000000000000000000000000000"),
Buy,
uint256.NewInt(10),
uint256.NewInt(10),
1234567890,
)
o2 := NewOrder(
"order-2",
common.HexToAddress("0x0000000000000000000000000000000000000000"),
Buy,
uint256.NewInt(10),
uint256.NewInt(20),
1234567890,
)
if ot.MinPriceQueue() != nil || ot.MaxPriceQueue() != nil {
t.Fatal("invalid price levels")
}
el1, _ := ot.Append(o1)
if ot.MinPriceQueue() != ot.MaxPriceQueue() {
t.Fatal("invalid price levels")
}
el2, _ := ot.Append(o2)
if ot.Depth() != 2 {
t.Fatal("invalid depth")
}
if ot.Len() != 2 {
t.Fatal("invalid orders count")
}
t.Log(ot)
if ot.MinPriceQueue().Head() != el1 || ot.MinPriceQueue().Tail() != el1 ||
ot.MaxPriceQueue().Head() != el2 || ot.MaxPriceQueue().Tail() != el2 {
t.Fatal("invalid price levels")
}
if o, err := ot.Remove(el1); o != o1 || err != nil {
t.Fatal("invalid order")
}
if ot.MinPriceQueue() != ot.MaxPriceQueue() {
t.Fatal("invalid price levels")
}
t.Log(ot)
}
func TestOrderSideJSON(t *testing.T) {
data := NewOrderSide()
data.Append(NewOrder("one", common.HexToAddress("0x0000000000000000000000000000000000000000"), Buy, uint256.NewInt(11), uint256.NewInt(110), 1234567890))
data.Append(NewOrder("two", common.HexToAddress("0x0000000000000000000000000000000000000000"), Buy, uint256.NewInt(22), uint256.NewInt(220), 1234567890))
data.Append(NewOrder("three", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(33), uint256.NewInt(330), 1234567890))
data.Append(NewOrder("four", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(44), uint256.NewInt(440), 1234567890))
data.Append(NewOrder("five", common.HexToAddress("0x0000000000000000000000000000000000000000"), Buy, uint256.NewInt(11), uint256.NewInt(110), 1234567890))
data.Append(NewOrder("six", common.HexToAddress("0x0000000000000000000000000000000000000000"), Buy, uint256.NewInt(22), uint256.NewInt(220), 1234567890))
data.Append(NewOrder("seven", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(33), uint256.NewInt(330), 1234567890))
data.Append(NewOrder("eight", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(44), uint256.NewInt(440), 1234567890))
result, _ := json.Marshal(data)
t.Log(string(result))
data = NewOrderSide()
if err := json.Unmarshal(result, data); err != nil {
t.Fatal(err)
}
t.Log(data)
err := json.Unmarshal([]byte(`[{"side":"fake"}]`), &data)
if err == nil {
t.Fatal("can unmarshal unsupported value")
}
}
func TestPriceFinding(t *testing.T) {
os := NewOrderSide()
os.Append(NewOrder("five", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(5), uint256.NewInt(130), 1234567890))
os.Append(NewOrder("one", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(5), uint256.NewInt(170), 1234567890))
os.Append(NewOrder("eight", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(5), uint256.NewInt(100), 1234567890))
os.Append(NewOrder("two", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(5), uint256.NewInt(160), 1234567890))
os.Append(NewOrder("four", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(5), uint256.NewInt(140), 1234567890))
os.Append(NewOrder("six", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(5), uint256.NewInt(120), 1234567890))
os.Append(NewOrder("three", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(5), uint256.NewInt(150), 1234567890))
os.Append(NewOrder("seven", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(5), uint256.NewInt(110), 1234567890))
if os.Volume().Cmp(uint256.NewInt(40)) != 0 {
t.Fatal("invalid volume")
}
if os.LessThan(uint256.NewInt(101)).Price().Cmp(uint256.NewInt(100)) != 0 ||
os.LessThan(uint256.NewInt(150)).Price().Cmp(uint256.NewInt(140)) != 0 ||
os.LessThan(uint256.NewInt(100)) != nil {
t.Fatal("LessThan return invalid price")
}
if os.GreaterThan(uint256.NewInt(169)).Price().Cmp(uint256.NewInt(170)) != 0 ||
os.GreaterThan(uint256.NewInt(150)).Price().Cmp(uint256.NewInt(160)) != 0 ||
os.GreaterThan(uint256.NewInt(170)) != nil {
t.Fatal("GreaterThan return invalid price")
}
t.Log(os.LessThan(uint256.NewInt(101)))
t.Log(os.GreaterThan(uint256.NewInt(169)))
}
func BenchmarkOrderSide(b *testing.B) {
ot := NewOrderSide()
stopwatch := time.Now()
var o *Order
for i := 0; i < b.N; i++ {
o = NewOrder(
fmt.Sprintf("order-%d", i),
common.HexToAddress("0x0000000000000000000000000000000000000000"),
Buy,
uint256.NewInt(10),
uint256.NewInt(uint64(i)),
uint64(stopwatch.UnixMilli()),
)
ot.Append(o)
}
elapsed := time.Since(stopwatch)
fmt.Printf("\n\nElapsed: %s\nTransactions per second: %f\n", elapsed, float64(b.N)/elapsed.Seconds())
}
package orderbook
import (
"encoding/json"
"reflect"
)
// Side 订单方向
type Side int
// Sell (卖出/卖单) 或 Buy (买入/买单)
const (
Sell Side = iota
Buy
)
// String 实现fmt.Stringer接口
func (s Side) String() string {
if s == Buy {
return "buy"
}
return "sell"
}
// MarshalJSON 实现json.Marshaler接口
func (s Side) MarshalJSON() ([]byte, error) {
return []byte(`"` + s.String() + `"`), nil
}
// UnmarshalJSON 实现json.Unmarshaler接口
func (s *Side) UnmarshalJSON(data []byte) error {
switch string(data) {
case `"buy"`:
*s = Buy
case `"sell"`:
*s = Sell
default:
return &json.UnsupportedValueError{
Value: reflect.New(reflect.TypeOf(data)),
Str: string(data),
}
}
return nil
}
package orderbook
import (
"encoding/json"
"testing"
)
func TestSideJSON(t *testing.T) {
data := struct {
S Side `json:"side"`
}{}
data.S = Buy
resultBuy, _ := json.Marshal(data)
t.Log(string(resultBuy))
data.S = Sell
resultSell, _ := json.Marshal(&data)
t.Log(string(resultSell))
_ = json.Unmarshal(resultBuy, &data)
t.Log(data)
_ = json.Unmarshal(resultSell, &data)
t.Log(data)
err := json.Unmarshal([]byte(`{"side":"fake"}`), &data)
if err == nil {
t.Fatal("can unmarshal unsupported value")
}
}
package types
type Coin string
const (
USDT Coin = "USDT"
BTC Coin = "BTC"
)
package types
import (
"bytes"
"encoding/gob"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
)
type OrderType string
const (
CancelOrder OrderType = "cancel order"
PlaceOrder OrderType = "place order"
)
type OrderAction string
const (
OrderActionBuy OrderAction = "buy"
OrderActionSell OrderAction = "sell"
OrderActionCancel OrderAction = "cancel"
OrderActionSignProxy OrderAction = "approve agent"
)
type Transaction interface {
Dump() ([]byte, error)
Load(data []byte) error
GetUser() common.Address
GetLimitPrice() *uint256.Int
GetPair() string
GetAction() OrderAction
GetQuality() *uint256.Int
GetOrderID() string
GetNonce() []byte
GetProxyAddress() common.Address
}
type Tx struct {
OrderID string
Time uint64
User common.Address
BaseCoin Coin
QuoteCoin Coin
Type OrderType
Action OrderAction
Nonce uint64
}
type CancelOrderTx struct {
Tx
}
func (tx *CancelOrderTx) Dump() ([]byte, error) {
var buffer bytes.Buffer
encoder := gob.NewEncoder(&buffer)
err := encoder.Encode(tx)
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func (tx *CancelOrderTx) Load(data []byte) error {
decoder := gob.NewDecoder(bytes.NewBuffer(data))
return decoder.Decode(tx)
}
func (tx *CancelOrderTx) GetUser() common.Address {
return tx.User
}
func (tx *CancelOrderTx) GetLimitPrice() *uint256.Int {
return nil
}
func (tx *CancelOrderTx) GetPair() string {
return string(tx.BaseCoin) + string(tx.QuoteCoin)
}
func (tx *CancelOrderTx) GetAction() OrderAction {
return OrderActionCancel
}
func (tx *CancelOrderTx) GetQuality() *uint256.Int {
return nil
}
func (tx *CancelOrderTx) GetOrderID() string {
return tx.OrderID
}
func (tx *CancelOrderTx) GetNonce() []byte {
return uint256.NewInt(tx.Nonce).Bytes()
}
func (tx *CancelOrderTx) GetProxyAddress() common.Address {
return common.Address{}
}
type PlaceOrderTx struct {
Tx
LimitPrice *uint256.Int
Quantity *uint256.Int
}
func (tx *PlaceOrderTx) Dump() ([]byte, error) {
var buffer bytes.Buffer
encoder := gob.NewEncoder(&buffer)
err := encoder.Encode(tx)
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func (tx *PlaceOrderTx) Load(data []byte) error {
decoder := gob.NewDecoder(bytes.NewBuffer(data))
return decoder.Decode(tx)
}
func (tx *PlaceOrderTx) GetUser() common.Address {
return tx.User
}
func (tx *PlaceOrderTx) GetLimitPrice() *uint256.Int {
return tx.LimitPrice
}
func (tx *PlaceOrderTx) GetPair() string {
return string(tx.BaseCoin) + string(tx.QuoteCoin)
}
func (tx *PlaceOrderTx) GetAction() OrderAction {
return tx.Action
}
func (tx *PlaceOrderTx) GetQuality() *uint256.Int {
return tx.Quantity
}
func (tx *PlaceOrderTx) GetOrderID() string {
return tx.OrderID
}
func (tx *PlaceOrderTx) GetNonce() []byte {
return uint256.NewInt(tx.Nonce).Bytes()
}
func (tx *PlaceOrderTx) GetProxyAddress() common.Address {
return common.Address{}
}
type SignProxyTx struct {
Tx
ProxyAddress common.Address
}
func (tx *SignProxyTx) Dump() ([]byte, error) {
var buffer bytes.Buffer
encoder := gob.NewEncoder(&buffer)
err := encoder.Encode(tx)
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func (tx *SignProxyTx) Load(data []byte) error {
decoder := gob.NewDecoder(bytes.NewBuffer(data))
return decoder.Decode(tx)
}
func (tx *SignProxyTx) GetUser() common.Address {
return tx.User
}
func (tx *SignProxyTx) GetLimitPrice() *uint256.Int {
return nil
}
func (tx *SignProxyTx) GetPair() string {
return ""
}
func (tx *SignProxyTx) GetAction() OrderAction {
return OrderActionSignProxy
}
func (tx *SignProxyTx) GetQuality() *uint256.Int {
return nil
}
func (tx *SignProxyTx) GetOrderID() string {
return ""
}
func (tx *SignProxyTx) GetNonce() []byte {
return uint256.NewInt(tx.Nonce).Bytes()
}
func (tx *SignProxyTx) GetProxyAddress() common.Address {
return tx.ProxyAddress
}
package utils_test
import (
"bytes"
"encoding/gob"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
)
func TestGob(t *testing.T) {
type Order struct {
Creator common.Address
Id string
Timestamp time.Time
Quantity *uint256.Int
Price *uint256.Int
Filled *uint256.Int
}
var buf bytes.Buffer
o := &Order{
Creator: common.HexToAddress("0x0000000000000000000000000000000000000001"),
Id: "order-1",
Timestamp: time.Now(),
Quantity: uint256.NewInt(10),
Price: uint256.NewInt(1),
Filled: uint256.NewInt(0),
}
enc := gob.NewEncoder(&buf)
err := enc.Encode(o)
if err != nil {
t.Fatal(err)
}
dec := gob.NewDecoder(&buf)
b := &Order{
}
err = dec.Decode(&b)
if err != nil {
t.Fatal(err)
}
t.Log("111", b.Quantity)
}
package utils
import (
"time"
"github.com/bwmarrin/snowflake"
)
var node *snowflake.Node
func init() {
node, _ = snowflake.NewNode(1)
}
func GenOrderID() string {
return node.Generate().String()
}
func NonceToTime(nonce uint64) time.Time {
return time.UnixMilli(int64(nonce))
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment