diff --git a/abci/example/orderbook/doc.go b/abci/example/orderbook/doc.go index 1e524afb3..b2515d7ca 100644 --- a/abci/example/orderbook/doc.go +++ b/abci/example/orderbook/doc.go @@ -1,4 +1,19 @@ //go:generate go install google.golang.org/protobuf/cmd/protoc-gen-go@latest //go:generate protoc -I. -I../.. --go_out=. --go_opt=paths=source_relative wire.proto msgs.proto +// The orderbook presents a more advanced example of a Tendermint application than the simple kvstore +// +// An orderbook is a tool used in financial markets for enabling trading of various commodities. Without +// delving into too much detail, an orderbook is made of two types of transactions: Bids and Asks. An Ask +// is an offer by a seller for n amount of a commodity at an AskPrice and a bid is an offer from a buyer +// for m amount of a commodity at a BidPrice. When the bid price exceeds the ask price, and the buyer quantity +// is less than or equal to the sellers quantity, the order is matched. In actual terms, we neglect the +// underlying denomination (i.e. USD) and effectively both participants are simultaneously a buyer and seller. +// +// This example falls far short of being a decentralized orderbook, but demonstrates how one can build an +// app-side mempool, how one can use PrepareProposal and ProcessProposal to craft complex transactions, +// how we can use signatures and validate transactions against state. How applications can manage concurrency, +// and demonstrate the lifecycle of transactions from RPC -> CheckTx -> Mempool -> PrepareProposal -> ProcessProposal +// -> DeliverTx -> Commit -> Querying + package orderbook diff --git a/abci/example/orderbook/market.go b/abci/example/orderbook/market.go index a5d17fc4f..1a99cd2ce 100644 --- a/abci/example/orderbook/market.go +++ b/abci/example/orderbook/market.go @@ -2,10 +2,14 @@ package orderbook import ( "container/heap" + sync "sync" ) type Market struct { - pair *Pair // i.e. EUR/USD (a market is bidirectional) + // immutable + pair *Pair // i.e. EUR/USD (a market is bidirectional) + + mtx sync.RWMutex askOrders *AskOrders // i.e. buying EUR for USD lowestAsk float64 bidOrders *BidOrders // i.e. selling EUR for USD or buying USD for EUR @@ -19,6 +23,8 @@ func NewMarket(p *Pair) *Market { } func (m *Market) AddBid(b *OrderBid) { + m.mtx.Lock() + defer m.mtx.Unlock() heap.Push(m.bidOrders, b) if b.MaxPrice > m.highestBid { m.highestBid = b.MaxPrice @@ -26,6 +32,8 @@ func (m *Market) AddBid(b *OrderBid) { } func (m *Market) AddAsk(a *OrderAsk) { + m.mtx.Lock() + defer m.mtx.Unlock() heap.Push(m.askOrders, a) if a.AskPrice < m.lowestAsk || m.lowestAsk == 0 { m.lowestAsk = a.AskPrice @@ -36,6 +44,8 @@ func (m *Market) AddAsk(a *OrderAsk) { // A bid matches an ask when the MaxPrice is greater than the AskPrice // and the MaxQuantity is greater than the quantity. func (m *Market) Match() *TradeSet { + m.mtx.Lock() + defer m.mtx.Unlock() // if one side doesn't have any orders than there is nothing to match // and we return early if m.askOrders.Len() == 0 || m.bidOrders.Len() == 0 { @@ -147,13 +157,37 @@ OUTER_LOOP: } func (m Market) LowestAsk() float64 { + m.mtx.RLock() + defer m.mtx.RUnlock() return m.lowestAsk } func (m Market) HighestBid() float64 { + m.mtx.RLock() + defer m.mtx.RUnlock() return m.highestBid } +func (m Market) GetBids() []OrderBid { + m.mtx.RLock() + defer m.mtx.RUnlock() + orders := make([]OrderBid, m.bidOrders.Len()) + for idx, order := range *m.bidOrders { + orders[idx] = *order + } + return orders +} + +func (m Market) GetAsks() []OrderAsk { + m.mtx.RLock() + defer m.mtx.RUnlock() + orders := make([]OrderAsk, m.askOrders.Len()) + for idx, order := range *m.askOrders { + orders[idx] = *order + } + return orders +} + // Heap ordered by lowest price type AskOrders []*OrderAsk diff --git a/abci/example/orderbook/query.go b/abci/example/orderbook/query.go new file mode 100644 index 000000000..649dd1190 --- /dev/null +++ b/abci/example/orderbook/query.go @@ -0,0 +1,31 @@ +package orderbook + +// Query the state of an account (returns a concrete copy) +func (sm *StateMachine) Account(id uint64) Account { + if int(id) >= len(sm.accounts) { + return Account{} + } + return *sm.accounts[id] +} + +// Query all the pairs that the orderbook has (returns a concrete copy) +func (sm *StateMachine) Pairs() []Pair { + pairs := make([]Pair, len(sm.pairs)) + idx := 0 + for _, pair := range sm.pairs { + pairs[idx] = *pair + idx++ + } + return pairs +} + +// Query the current orders for a pair (returns concrete copies) +func (sm *StateMachine) Orders(pair *Pair) ([]OrderBid, []OrderAsk) { + market, ok := sm.markets[pair.String()] + if !ok { + return nil, nil + } + return market.GetBids(), market.GetAsks() +} + +func (sm *StateMachine) Height() int64 { return sm.lastHeight }