orderbook example

This commit is contained in:
Sam Ricotta
2022-11-14 14:54:52 +01:00
parent e1f3d5c58c
commit 38897de6d7
4 changed files with 165 additions and 90 deletions
+87 -62
View File
@@ -1,6 +1,7 @@
package orderbook
import (
"crypto/ed25519"
"fmt"
"github.com/cosmos/gogoproto/proto"
@@ -13,17 +14,23 @@ var _ types.Application = (*StateMachine)(nil)
const Version = 1
//TO DO: Error codes
const (
// In tendermint a zero code is okay and all non zero codes are errors
StatusOK = iota
ErrDecoding
ErrUnknownMessage
ErrValidateBasic
ErrNoAccount
ErrNoPair
)
type StateMachine struct {
// persisted state
db dbm.DB
// in-memory state
accounts map[uint64]*Account
// ephemeral state (not used for the app hash) but for
// convienience
accounts map[uint64]*Account
pairs map[string]struct{} // lookup pairs
commodities map[string]struct{} // lookup commodities
publicKeys map[string]struct{} // lookup existence of an account
@@ -34,12 +41,13 @@ type StateMachine struct {
func New(db dbm.DB) *StateMachine {
// execute a database call that fetches the data for accounts
StateMachine := StateMachine {
accounts: map[uint64]*Account{},
pairs: map[string]struct{},
commodities: map[string]struct{},
publicKeys: map[string]struct{},
markets: map[string]*Market{},
StateMachine := StateMachine{
accounts: make(map[uint64]*Account),
pairs: make(map[string]struct{}),
commodities: make(map[string]struct{}),
publicKeys: make(map[string]struct{}),
markets: make(map[string]*Market),
db: db,
}
return &StateMachine
}
@@ -54,79 +62,59 @@ func (sm *StateMachine) CheckTx(req types.RequestCheckTx) types.ResponseCheckTx
err := proto.Unmarshal(req.Tx, msg)
if err != nil {
return types.ResponseCheckTx{Code: 1} // decoding error
return types.ResponseCheckTx{Code: ErrDecoding, Log: err.Error()} // decoding error
}
// validations for each msg below
switch m := msg.Sum.(type) {
case *Msg_MsgRegisterPair:
if err := m.MsgRegisterPair.ValidateBasic(); err != nil {
return types.ResponseCheckTx{Code: 3}
return types.ResponseCheckTx{Code: ErrValidateBasic, Log: err.Error()}
}
case *Msg_MsgCreateAccount:
if err := m.MsgCreateAccount.ValidateBasic(); err != nil {
return types.ResponseCheckTx{Code: 3}
return types.ResponseCheckTx{Code: ErrValidateBasic, Log: err.Error()}
}
case *Msg_MsgBid:
if err := m.MsgBid.ValidateBasic(); err != nil {
return types.ResponseCheckTx{Code: 3, Log: err.Error()}
return types.ResponseCheckTx{Code: ErrValidateBasic, Log: err.Error()}
}
// check if account exists
if _, ok := sm.accounts[m.MsgBid.BidOrder.OwnerId]; !ok {
return types.ResponseCheckTx{Code: 4}
return types.ResponseCheckTx{Code: ErrNoAccount}
}
// check the pair exists
if _, ok := sm.pairs[m.MsgBid.Pair.String()]; !ok {
return types.ResponseCheckTx{Code: 4}
return types.ResponseCheckTx{Code: ErrNoPair}
}
case *Msg_MsgAsk:
if err := m.MsgAsk.ValidateBasic(); err != nil {
return types.ResponseCheckTx{Code: 3, Log: err.Error()}
return types.ResponseCheckTx{Code: ErrValidateBasic, Log: err.Error()}
}
// check if account exists
account, ok := sm.accounts[m.MsgAsk.AskOrder.OwnerId]
_, ok := sm.accounts[m.MsgAsk.AskOrder.OwnerId]
if !ok {
return types.ResponseCheckTx{Code: 4}
return types.ResponseCheckTx{Code: ErrNoAccount}
}
// check the pair exists
if _, ok := sm.pairs[m.MsgAsk.Pair.String()]; !ok {
return types.ResponseCheckTx{Code: 4}
}
// check if pair is registered
if _, ok := sm.markets[m.MsgAsk.Pair.String()]; !ok {
return types.ResponseCheckTx{Code: 4}
}
// check the account has a enough quantity
found := false
for _, commodity := range account.Commodities {
if commodity.Denom == m.MsgAsk.Pair.SellersDenomination {
if m.MsgAsk.AskOrder.Quantity > commodity.Quantity {
return types.ResponseCheckTx{Code: 4}
}
found = true
}
}
if !found {
return types.ResponseCheckTx{Code: 4}
return types.ResponseCheckTx{Code: ErrNoPair}
}
default:
return types.ResponseCheckTx{Code: 2} // unknown message type
return types.ResponseCheckTx{Code: ErrUnknownMessage} // unknown message type
}
return types.ResponseCheckTx{Code: 0}
return types.ResponseCheckTx{Code: StatusOK}
}
func (sm *StateMachine) Commit() types.ResponseCommit {
@@ -280,8 +268,11 @@ func (sm *StateMachine) ProcessProposal(req types.RequestProcessProposal) types.
return rejectProposal()
}
case *Msg_MsgAsk, *Msg_MsgBid:
// TODO: verify the signature
case *Msg_MsgAsk, *Msg_MsgBid: // MsgAsk and MsgBid are not allowed individually - they need to be matched as a TradeSet
return rejectProposal()
case *Msg_MsgCreateAccount:
if err := m.MsgCreateAccount.ValidateBasic(); err != nil {
return rejectProposal()
@@ -292,6 +283,8 @@ func (sm *StateMachine) ProcessProposal(req types.RequestProcessProposal) types.
return rejectProposal()
}
// TODO: verify the signature
case *Msg_MsgTradeSet:
// for each matched order
// check the accounts exist, that the signatures are valid and that they have the available funds to make the swap
@@ -299,6 +292,24 @@ func (sm *StateMachine) ProcessProposal(req types.RequestProcessProposal) types.
return rejectProposal()
}
// check the pair exists
if _, ok := sm.pairs[m.MsgTradeSet.TradeSet.Pair.String()]; !ok {
return rejectProposal()
}
for _, order := range m.MsgTradeSet.TradeSet.MatchedOrders {
if !sm.isMatchedOrderValid(order, m.MsgTradeSet.TradeSet.Pair) {
return rejectProposal()
}
// bidOwner := sm.accounts[order.OrderBid.OwnerId]
// askOwner := sm.accounts[order.OrderAsk.OwnerId]
// ed25519.Verify(bidOwner.PublicKey, )
}
default:
return rejectProposal()
@@ -312,25 +323,7 @@ func (sm *StateMachine) validateTradeSetAgainstState(tradeSet *TradeSet) *TradeS
output := &TradeSet{Pair: tradeSet.Pair}
for _, matchedOrder := range tradeSet.MatchedOrders {
bidOwner := sm.accounts[matchedOrder.OrderBid.OwnerId]
askOwner := sm.accounts[matchedOrder.OrderAsk.OwnerId]
askCommodities := askOwner.FindCommidity(tradeSet.Pair.SellersDenomination)
if askCommodities == nil {
continue
}
buyCommodities := bidOwner.FindCommidity(tradeSet.Pair.BuyersDenomination)
if buyCommodities == nil {
continue
}
// Seller has enough of the commodity
if askCommodities.Quantity-matchedOrder.OrderAsk.Quantity < 0 {
continue
}
// Buyer has enough of the buying commodity
if buyCommodities.Quantity-(matchedOrder.OrderAsk.AskPrice*matchedOrder.OrderAsk.Quantity) < 0 {
if !sm.isMatchedOrderValid(matchedOrder, tradeSet.Pair) {
continue
}
@@ -341,6 +334,38 @@ func (sm *StateMachine) validateTradeSetAgainstState(tradeSet *TradeSet) *TradeS
return output
}
func (sm *StateMachine) isMatchedOrderValid(order *MatchedOrder, pair *Pair) bool {
bidOwner, exists := sm.accounts[order.OrderBid.OwnerId]
if !exists {
return false
}
askOwner, exists := sm.accounts[order.OrderAsk.OwnerId]
if !exists {
return false
}
askCommodities := askOwner.FindCommidity(pair.SellersDenomination)
if askCommodities == nil {
return false
}
buyCommodities := bidOwner.FindCommidity(pair.BuyersDenomination)
if buyCommodities == nil {
return false
}
// Seller has enough of the commodity
if askCommodities.Quantity-order.OrderAsk.Quantity < 0 {
return false
}
// Buyer has enough of the buying commodity
if buyCommodities.Quantity-(order.OrderAsk.AskPrice*order.OrderAsk.Quantity) < 0 {
return false
}
return true
}
func rejectProposal() types.ResponseProcessProposal {
return types.ResponseProcessProposal{Status: types.ResponseProcessProposal_REJECT}
}
+66 -22
View File
@@ -3,35 +3,79 @@ package orderbook_test
import (
"testing"
"github.com/cosmos/gogoproto/proto"
"github.com/stretchr/testify/require"
dbm "github.com/tendermint/tm-db"
"github.com/tendermint/tendermint/abci/example/orderbook"
"github.com/tendermint/tendermint/abci/types"
)
func TestNew(t *testing.T) {
StateMachine := New()
func TestCheckTx(t *testing.T) {
app := orderbook.New(dbm.NewMemDB())
// initialise market
market := orderbook.NewMarket(testPair)
// create transaction
response := S.PrepareProposal(types.RequestPrepareProposal{})
testCases := []struct {
name string
msg *orderbook.Msg
responseCode int
}{
{
name: "test empty tx",
msg: &orderbook.Msg{},
responseCode: orderbook.ErrUnknownMessage,
},
{
name: "test msg ask",
msg: &orderbook.Msg{Sum: &orderbook.Msg_MsgAsk{MsgAsk: &orderbook.MsgAsk{
Pair: testPair,
AskOrder: &orderbook.OrderAsk{
Quantity: 10,
AskPrice: 1,
OwnerId: 1,
Signature: []byte("signature"),
},
}}},
responseCode: orderbook.StatusOK,
},
{
name: "test msg bid",
msg: &orderbook.Msg{Sum: &orderbook.Msg_MsgBid{MsgBid: &orderbook.MsgBid{
Pair: testPair,
OrderBid: &orderbook.OrderBid{
MaxQuantity: 15,
MaxPrice: 5,
OwnerId: 1,
Signature: []byte("signature"),
},
}},
responseCode: orderbook.StatusOK,
},
{
name: "test msg register pair",
msg: &orderbook.Msg{Sum: &orderbook.Msg_MsgRegisterPair{MsgRegisterPair: &orderbook.MsgRegisterPair{
Pair: testPair,
}},
responseCode: orderbook.StatusOK,
}
// check to see if the market collected all of the pairs
// test to see if the tradeset is valid
// require.EqualValues(t, )
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
bz, err := proto.Marshal(tc.msg)
require.NoError(t, err)
resp := app.CheckTx(types.RequestCheckTx{Tx: bz})
require.Equal(t, tc.responseCode, resp.Code)
})
}
}
func TestNew(t *testing.T) {
var b map[string]string
var a StateMachine
// func TestPrepareProposal(t *testing.T) {
// app := orderbook.New(dbm.NewMemDB())
// }
// func TestProcessProposal(t *testing.T) {
// app := orderbook.New(dbm.NewMemDB())
// }
}
type B struct {
Field string
anotherField []int
anotherField2 [10]int
}
// func TestFinalizeBlock(t *testing.T) {
// app := orderbook.New(dbm.NewMemDB())
// }
+5 -6
View File
@@ -1,12 +1,11 @@
package orderbook
import dbm "github.com/tendermint/tm-db"
type AccountStore struct {
db dbm.DB
}
type AccountStore struct {
db dbm.DB
}
// iterate over the account database
// Add to the account database
// Add to the account database
// find an account
+7
View File
@@ -100,6 +100,7 @@ func (o *OrderBid) ValidateBasic() error {
return nil
}
// check signatures are valid
func (m *MatchedOrder) ValidateBasic() error {
@@ -139,7 +140,11 @@ func (o *OrderAsk) ValidateBasic() error {
if o.AskPrice <= 0 {
return errors.New("min price must be greater than 0")
}
return nil
}
func (o *OrderAsk) DeterministicSignatureBytes(pair *Pair) []byte {
// buf := bytes.NewBuffer()
return nil
}
@@ -149,5 +154,7 @@ func (a *Account) FindCommidity(denom string) *Commodity {
return c
}
}
return nil
}