From d90c2009961fd7e68c1a0173d18db6cd77e3dd43 Mon Sep 17 00:00:00 2001 From: erhenglu <42333959+erhenglu@users.noreply.github.com> Date: Tue, 2 Jun 2020 17:01:14 +0800 Subject: [PATCH] BEP8 Mini token features (#725) *support mini-token --- admin/tx.go | 2 + app/app.go | 52 +- app/app_pub_test.go | 10 +- app/apptest/match_allocation_test.go | 2 + app/apptest/ordertx_test.go | 12 +- app/config/config.go | 12 +- app/helpers.go | 11 +- app/pub/helpers.go | 170 +++-- app/pub/keeper_pub_test.go | 102 +-- app/pub/msgs.go | 2 + app/pub/publisher.go | 30 +- app/pub/types.go | 3 +- app_test/abci_open_orders_test.go | 102 ++- app_test/utils_test.go | 24 +- common/testutils/testutils.go | 21 +- common/types/mini_token.go | 212 ++++++ common/types/token.go | 70 +- common/types/token_test.go | 24 +- common/types/wire.go | 5 +- common/upgrade/upgrade.go | 8 +- go.mod | 4 +- go.sum | 8 +- integration_test.sh | 60 +- networks/demo/issue_mini.exp | 21 + networks/demo/list_mini.exp | 19 + networks/demo/multi_send.exp | 3 +- networks/publisher/list_mini.sh | 36 + networks/publisher/newIndexer.sh | 14 +- networks/publisher/newPublisher.sh | 14 +- networks/publisher/newValidator.sh | 8 +- networks/publisher/ordergen.sh | 31 +- networks/publisher/setup.sh | 16 +- networks/publisher/setup_mini.sh | 114 ++++ networks/tools/snapshot_viewer/snapshot.go | 1 + plugins/api/handlers.go | 21 +- plugins/api/routes.go | 11 +- plugins/api/server.go | 8 +- plugins/dex/abci.go | 50 +- plugins/dex/aliases.go | 6 +- plugins/dex/client/cli/commands.go | 1 + plugins/dex/client/cli/list.go | 53 +- plugins/dex/client/rest/getpairs.go | 8 +- plugins/dex/list/handler.go | 10 +- plugins/dex/list/handler_mini.go | 46 ++ plugins/dex/list/handler_mini_test.go | 118 ++++ plugins/dex/list/handler_test.go | 22 +- plugins/dex/list/hooks.go | 12 +- plugins/dex/list/hooks_test.go | 18 +- plugins/dex/list/msg.go | 4 +- plugins/dex/list/msg_mini.go | 75 +++ plugins/dex/list/msg_mini_test.go | 67 ++ plugins/dex/matcheng/engine_new.go | 25 +- plugins/dex/matcheng/match_new_test.go | 17 +- plugins/dex/order/fee.go | 2 +- plugins/dex/order/fee_test.go | 163 +++-- plugins/dex/order/handler.go | 142 ++-- plugins/dex/order/handler_test.go | 38 +- plugins/dex/order/keeper.go | 726 ++++++++++----------- plugins/dex/order/keeper_match.go | 178 +++++ plugins/dex/order/keeper_recovery.go | 65 +- plugins/dex/order/keeper_test.go | 204 +++++- plugins/dex/order/mini_keeper.go | 98 +++ plugins/dex/order/openOrders_test.go | 2 +- plugins/dex/order/order_keeper.go | 289 ++++++++ plugins/dex/order/quickselect.go | 57 ++ plugins/dex/order/quickselect_test.go | 119 ++++ plugins/dex/order/symbol_selector.go | 90 +++ plugins/dex/order/transfer.go | 1 + plugins/dex/order/types.go | 6 + plugins/dex/plugin.go | 27 +- plugins/dex/route.go | 7 +- plugins/dex/store/codec.go | 8 +- plugins/dex/store/mapper.go | 14 +- plugins/dex/store/utils.go | 14 +- plugins/dex/utils/pair.go | 9 + plugins/dex/wire.go | 2 + plugins/param/genesis.go | 6 + plugins/param/plugin.go | 14 + plugins/param/types/types.go | 5 + plugins/tokens/abci.go | 93 ++- plugins/tokens/abci_test.go | 12 +- plugins/tokens/burn/handler.go | 24 +- plugins/tokens/burn/handler_test.go | 159 +++++ plugins/tokens/burn/msg.go | 10 +- plugins/tokens/client/cli/commands.go | 8 + plugins/tokens/client/cli/helper.go | 26 +- plugins/tokens/client/cli/info.go | 13 +- plugins/tokens/client/cli/issue.go | 34 +- plugins/tokens/client/cli/issue_mini.go | 80 +++ plugins/tokens/client/cli/issue_tiny.go | 64 ++ plugins/tokens/client/cli/seturi_mini.go | 43 ++ plugins/tokens/client/rest/gettoken.go | 25 +- plugins/tokens/client/rest/gettokens.go | 20 +- plugins/tokens/freeze/handler.go | 21 +- plugins/tokens/freeze/handler_test.go | 209 ++++++ plugins/tokens/freeze/msg.go | 19 +- plugins/tokens/genesis.go | 2 +- plugins/tokens/issue/handler.go | 133 ++-- plugins/tokens/issue/handler_mini.go | 55 ++ plugins/tokens/issue/handler_mini_test.go | 140 ++++ plugins/tokens/issue/handler_test.go | 19 +- plugins/tokens/issue/msg.go | 13 +- plugins/tokens/issue/msg_mini.go | 80 +++ plugins/tokens/issue/msg_tiny.go | 80 +++ plugins/tokens/plugin.go | 11 +- plugins/tokens/route.go | 2 + plugins/tokens/seturi/handler.go | 60 ++ plugins/tokens/seturi/handler_test.go | 91 +++ plugins/tokens/seturi/msg.go | 56 ++ plugins/tokens/store/mapper.go | 110 ++-- plugins/tokens/store/mapper_mini.go | 52 ++ plugins/tokens/swap/handler_test.go | 12 +- plugins/tokens/swap/msg.go | 16 + plugins/tokens/timelock/msgs.go | 15 + plugins/tokens/tokens.go | 1 + plugins/tokens/wire.go | 4 + 116 files changed, 4773 insertions(+), 1120 deletions(-) create mode 100644 common/types/mini_token.go create mode 100755 networks/demo/issue_mini.exp create mode 100755 networks/demo/list_mini.exp create mode 100755 networks/publisher/list_mini.sh create mode 100755 networks/publisher/setup_mini.sh create mode 100644 plugins/dex/list/handler_mini.go create mode 100644 plugins/dex/list/handler_mini_test.go create mode 100644 plugins/dex/list/msg_mini.go create mode 100644 plugins/dex/list/msg_mini_test.go create mode 100644 plugins/dex/order/keeper_match.go create mode 100644 plugins/dex/order/mini_keeper.go create mode 100644 plugins/dex/order/order_keeper.go create mode 100644 plugins/dex/order/quickselect.go create mode 100644 plugins/dex/order/quickselect_test.go create mode 100644 plugins/dex/order/symbol_selector.go create mode 100644 plugins/tokens/burn/handler_test.go create mode 100644 plugins/tokens/client/cli/issue_mini.go create mode 100644 plugins/tokens/client/cli/issue_tiny.go create mode 100644 plugins/tokens/client/cli/seturi_mini.go create mode 100644 plugins/tokens/freeze/handler_test.go create mode 100644 plugins/tokens/issue/handler_mini.go create mode 100644 plugins/tokens/issue/handler_mini_test.go create mode 100644 plugins/tokens/issue/msg_mini.go create mode 100644 plugins/tokens/issue/msg_tiny.go create mode 100644 plugins/tokens/seturi/handler.go create mode 100644 plugins/tokens/seturi/handler_test.go create mode 100644 plugins/tokens/seturi/msg.go create mode 100644 plugins/tokens/store/mapper_mini.go diff --git a/admin/tx.go b/admin/tx.go index 49633a4de..889554563 100644 --- a/admin/tx.go +++ b/admin/tx.go @@ -25,6 +25,8 @@ var transferOnlyModeBlackList = []string{ timelock.TimeLockMsg{}.Type(), timelock.TimeUnlockMsg{}.Type(), timelock.TimeRelockMsg{}.Type(), + issue.IssueMiniMsg{}.Type(), + issue.IssueTinyMsg{}.Type(), } var TxBlackList = map[runtime.Mode][]string{ diff --git a/app/app.go b/app/app.go index be0bb4317..e33086009 100644 --- a/app/app.go +++ b/app/app.go @@ -42,7 +42,8 @@ import ( "github.com/binance-chain/node/plugins/param" "github.com/binance-chain/node/plugins/param/paramhub" "github.com/binance-chain/node/plugins/tokens" - tkstore "github.com/binance-chain/node/plugins/tokens/store" + "github.com/binance-chain/node/plugins/tokens/issue" + "github.com/binance-chain/node/plugins/tokens/seturi" "github.com/binance-chain/node/plugins/tokens/swap" "github.com/binance-chain/node/plugins/tokens/timelock" "github.com/binance-chain/node/wire" @@ -79,7 +80,7 @@ type BinanceChain struct { CoinKeeper bank.Keeper DexKeeper *dex.DexKeeper AccountKeeper auth.AccountKeeper - TokenMapper tkstore.Mapper + TokenMapper tokens.Mapper ValAddrCache *ValAddrCache stakeKeeper stake.Keeper govKeeper gov.Keeper @@ -129,7 +130,7 @@ func NewBinanceChain(logger log.Logger, db dbm.DB, traceStore io.Writer, baseApp // mappers app.AccountKeeper = auth.NewAccountKeeper(cdc, common.AccountStoreKey, types.ProtoAppAccount) - app.TokenMapper = tkstore.NewMapper(cdc, common.TokenStoreKey) + app.TokenMapper = tokens.NewMapper(cdc, common.TokenStoreKey) app.CoinKeeper = bank.NewBaseKeeper(app.AccountKeeper) app.ParamHub = paramhub.NewKeeper(cdc, common.ParamsStoreKey, common.TParamsStoreKey) tradingPairMapper := dex.NewTradingPairMapper(app.Codec, common.PairStoreKey) @@ -265,6 +266,8 @@ func SetUpgradeConfig(upgradeConfig *config.UpgradeConfig) { upgrade.Mgr.AddUpgradeHeight(upgrade.LotSizeOptimization, upgradeConfig.LotSizeUpgradeHeight) upgrade.Mgr.AddUpgradeHeight(upgrade.ListingRuleUpgrade, upgradeConfig.ListingRuleUpgradeHeight) upgrade.Mgr.AddUpgradeHeight(upgrade.FixZeroBalance, upgradeConfig.FixZeroBalanceHeight) + + upgrade.Mgr.AddUpgradeHeight(upgrade.BEP8, upgradeConfig.BEP8Height) upgrade.Mgr.AddUpgradeHeight(upgrade.BEP67, upgradeConfig.BEP67Height) upgrade.Mgr.AddUpgradeHeight(upgrade.BEP70, upgradeConfig.BEP70Height) @@ -287,6 +290,13 @@ func SetUpgradeConfig(upgradeConfig *config.UpgradeConfig) { swap.ClaimHTLTMsg{}.Type(), swap.RefundHTLTMsg{}.Type(), ) + // register msg types of upgrade + upgrade.Mgr.RegisterMsgTypes(upgrade.BEP8, + issue.IssueMiniMsg{}.Type(), + issue.IssueTinyMsg{}.Type(), + seturi.SetURIMsg{}.Type(), + list.ListMiniMsg{}.Type(), + ) } func getABCIQueryBlackList(queryConfig *config.QueryConfig) map[string]bool { @@ -305,9 +315,8 @@ func (app *BinanceChain) initRunningMode() { } func (app *BinanceChain) initDex(pairMapper dex.TradingPairMapper) { - app.DexKeeper = dex.NewOrderKeeper(common.DexStoreKey, app.AccountKeeper, pairMapper, - app.RegisterCodespace(dex.DefaultCodespace), app.baseConfig.OrderKeeperConcurrency, - app.Codec, app.publicationConfig.ShouldPublishAny()) + + app.DexKeeper = dex.NewDexKeeper(common.DexStoreKey, app.AccountKeeper, pairMapper, app.RegisterCodespace(dex.DefaultCodespace), app.baseConfig.OrderKeeperConcurrency, app.Codec, app.publicationConfig.ShouldPublishAny()) app.DexKeeper.SubscribeParamChange(app.ParamHub) app.DexKeeper.SetBUSDSymbol(app.dexConfig.BUSDSymbol) @@ -330,11 +339,12 @@ func (app *BinanceChain) initDex(pairMapper dex.TradingPairMapper) { stateDB, app.LastBlockHeight(), app.TxDecoder) + } func (app *BinanceChain) initPlugins() { tokens.InitPlugin(app, app.TokenMapper, app.AccountKeeper, app.CoinKeeper, app.timeLockKeeper, app.swapKeeper) - dex.InitPlugin(app, app.DexKeeper, app.TokenMapper, app.AccountKeeper, app.govKeeper) + dex.InitPlugin(app, app.DexKeeper, app.TokenMapper, app.govKeeper) param.InitPlugin(app, app.ParamHub) account.InitPlugin(app, app.AccountKeeper) } @@ -525,12 +535,11 @@ func (app *BinanceChain) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) a isBreatheBlock := app.isBreatheBlock(height, lastBlockTime, blockTime) var tradesToPublish []*pub.Trade - if sdk.IsUpgrade(upgrade.BEP19) || !isBreatheBlock { if app.publicationConfig.ShouldPublishAny() && pub.IsLive { - tradesToPublish = pub.MatchAndAllocateAllForPublish(app.DexKeeper, ctx) + tradesToPublish = pub.MatchAndAllocateAllForPublish(app.DexKeeper, ctx, isBreatheBlock) } else { - app.DexKeeper.MatchAndAllocateAll(ctx, nil) + app.DexKeeper.MatchAndAllocateSymbols(ctx, nil, isBreatheBlock) } } @@ -550,6 +559,7 @@ func (app *BinanceChain) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) a } app.DexKeeper.StoreTradePrices(ctx) + blockFee := distributeFee(ctx, app.AccountKeeper, app.ValAddrCache, app.publicationConfig.PublishBlockFee) tags, passed, failed := gov.EndBlocker(ctx, app.govKeeper) @@ -768,10 +778,13 @@ func (app *BinanceChain) publish(tradesToPublish []*pub.Trade, proposalsToPublis var blockToPublish *pub.Block var latestPriceLevels order.ChangedPriceLevelsMap + orderChanges := app.DexKeeper.GetAllOrderChanges() + orderInfoForPublish := app.DexKeeper.GetAllOrderInfosForPub() + duration := pub.Timer(app.Logger, fmt.Sprintf("collect publish information, height=%d", height), func() { if app.publicationConfig.PublishAccountBalance { txRelatedAccounts := app.Pool.TxRelatedAddrs() - tradeRelatedAccounts := pub.GetTradeAndOrdersRelatedAccounts(app.DexKeeper, tradesToPublish) + tradeRelatedAccounts := pub.GetTradeAndOrdersRelatedAccounts(tradesToPublish, orderChanges, orderInfoForPublish) accountsToPublish = pub.GetAccountBalances( app.AccountKeeper, ctx, @@ -800,33 +813,34 @@ func (app *BinanceChain) publish(tradesToPublish []*pub.Trade, proposalsToPublis pub.Logger.Info("start to publish", "height", height, "blockTime", blockTime, "numOfTrades", len(tradesToPublish), "numOfOrders", // the order num we collected here doesn't include trade related orders - len(app.DexKeeper.OrderChanges), + len(orderChanges), "numOfProposals", proposalsToPublish.NumOfMsgs, "numOfStakeUpdates", stakeUpdates.NumOfMsgs, "numOfAccounts", len(accountsToPublish)) - pub.ToRemoveOrderIdCh = make(chan string, pub.ToRemoveOrderIdChannelSize) + pub.ToRemoveOrderIdCh = make(chan pub.OrderSymbolId, pub.ToRemoveOrderIdChannelSize) + pub.ToPublishCh <- pub.NewBlockInfoToPublish( height, blockTime, tradesToPublish, proposalsToPublish, stakeUpdates, - app.DexKeeper.OrderChanges, // thread-safety is guarded by the signal from RemoveDoneCh - app.DexKeeper.OrderInfosForPub, // thread-safety is guarded by the signal from RemoveDoneCh + orderChanges, // thread-safety is guarded by the signal from RemoveDoneCh + orderInfoForPublish, // thread-safety is guarded by the signal from RemoveDoneCh accountsToPublish, latestPriceLevels, blockFee, - app.DexKeeper.RoundOrderFees, + app.DexKeeper.RoundOrderFees, //only use DexKeeper RoundOrderFees transferToPublish, blockToPublish) // remove item from OrderInfoForPublish when we published removed order (cancel, iocnofill, fullyfilled, expired) - for id := range pub.ToRemoveOrderIdCh { - pub.Logger.Debug("delete order from order changes map", "orderId", id) - delete(app.DexKeeper.OrderInfosForPub, id) + for o := range pub.ToRemoveOrderIdCh { + pub.Logger.Debug("delete order from order changes map", "symbol", o.Symbol, "orderId", o.Id) + app.DexKeeper.RemoveOrderInfosForPub(o.Symbol, o.Id) } pub.Logger.Debug("finish publish", "height", height) diff --git a/app/app_pub_test.go b/app/app_pub_test.go index 8c36807d3..f4853ff70 100644 --- a/app/app_pub_test.go +++ b/app/app_pub_test.go @@ -98,7 +98,7 @@ func setupAppTest(t *testing.T) (*assert.Assertions, *require.Assertions, *Binan pub.IsLive = true keeper := app.DexKeeper - keeper.CollectOrderInfoForPublish = true + keeper.EnablePublish() tradingPair := dextypes.NewTradingPair("XYZ-000", "BNB", 102000) keeper.PairMapper.AddTradingPair(app.DeliverState.Ctx, tradingPair) keeper.AddEngine(tradingPair) @@ -112,8 +112,8 @@ func setupAppTest(t *testing.T) (*assert.Assertions, *require.Assertions, *Binan keeper.FeeManager.FeeConfig.CancelFee = 12 keeper.FeeManager.FeeConfig.CancelFeeNative = 6 - _, buyerAcc := testutils.NewAccountForPub(ctx, am, 100000000000, 0, 0) // give user enough coins to pay the fee - _, sellerAcc := testutils.NewAccountForPub(ctx, am, 100000000000, 0, 0) + _, buyerAcc := testutils.NewAccountForPub(ctx, am, 100000000000, 0, 0, "XYZ-000") // give user enough coins to pay the fee + _, sellerAcc := testutils.NewAccountForPub(ctx, am, 100000000000, 0, 0, "XYZ-000") return assert.New(t), require.New(t), app, buyerAcc, sellerAcc } @@ -140,7 +140,7 @@ func TestAppPub_MatchOrder(t *testing.T) { ctx := app.DeliverState.Ctx msg := orderPkg.NewNewOrderMsg(buyerAcc.GetAddress(), orderPkg.GenerateOrderID(1, buyerAcc.GetAddress()), orderPkg.Side.BUY, "XYZ-000_BNB", 102000, 300000000) - handler := orderPkg.NewHandler(app.GetCodec(), app.DexKeeper, app.AccountKeeper) + handler := orderPkg.NewHandler(app.DexKeeper) app.DeliverState.Ctx = app.DeliverState.Ctx.WithBlockHeight(41).WithBlockTime(time.Unix(0, 100)) buyerAcc.SetSequence(1) app.AccountKeeper.SetAccount(ctx, buyerAcc) @@ -214,7 +214,7 @@ func TestAppPub_MatchOrder(t *testing.T) { func TestAppPub_MatchAndCancelFee(t *testing.T) { assert, require, app, buyerAcc, sellerAcc := setupAppTest(t) - handler := orderPkg.NewHandler(app.GetCodec(), app.DexKeeper, app.AccountKeeper) + handler := orderPkg.NewHandler(app.DexKeeper) ctx := app.DeliverState.Ctx // ==== Place a to-be-matched sell order and a to-be-cancelled buy order (in different symbol) diff --git a/app/apptest/match_allocation_test.go b/app/apptest/match_allocation_test.go index 0d39725f7..5c9dbd936 100644 --- a/app/apptest/match_allocation_test.go +++ b/app/apptest/match_allocation_test.go @@ -127,6 +127,8 @@ func SetupTest(initPrices ...int64) (crypto.Address, sdk.Context, []sdk.Account) func SetupTest_new(initPrices ...int64) (crypto.Address, sdk.Context, []sdk.Account) { // for new match engine upgrade.Mgr.AddUpgradeHeight(upgrade.BEP19, -1) + upgrade.Mgr.AddUpgradeHeight(upgrade.BEP8, -1) + upgrade.Mgr.AddUpgradeHeight(upgrade.BEP70, -1) addr := secp256k1.GenPrivKey().PubKey().Address() accAddr := sdk.AccAddress(addr) baseAcc := auth.BaseAccount{Address: accAddr} diff --git a/app/apptest/ordertx_test.go b/app/apptest/ordertx_test.go index f8f593937..c84b36752 100644 --- a/app/apptest/ordertx_test.go +++ b/app/apptest/ordertx_test.go @@ -124,10 +124,12 @@ func Test_handleNewOrder_DeliverTx(t *testing.T) { tradingPair := types.NewTradingPair("BTC-000", "BNB", 1e8) testApp.DexKeeper.PairMapper.AddTradingPair(ctx, tradingPair) testApp.DexKeeper.AddEngine(tradingPair) + testApp.DexKeeper.GetEngines()["BTC-000_BNB"].LastMatchHeight = -1 tradingPair2 := types.NewTradingPair("ETH-001", "BNB", 1e8) testApp.DexKeeper.PairMapper.AddTradingPair(ctx, tradingPair2) testApp.DexKeeper.AddEngine(tradingPair2) + testApp.DexKeeper.GetEngines()["ETH-001_BNB"].LastMatchHeight = -1 add := Account(0).GetAddress() oid := fmt.Sprintf("%X-0", add) @@ -159,9 +161,11 @@ func Test_Match(t *testing.T) { ethPair := types.NewTradingPair("ETH-000", "BNB", 97e8) testApp.DexKeeper.PairMapper.AddTradingPair(ctx, ethPair) testApp.DexKeeper.AddEngine(ethPair) + testApp.DexKeeper.GetEngines()["ETH-000_BNB"].LastMatchHeight = -1 btcPair := types.NewTradingPair("BTC-000", "BNB", 96e8) testApp.DexKeeper.PairMapper.AddTradingPair(ctx, btcPair) testApp.DexKeeper.AddEngine(btcPair) + testApp.DexKeeper.GetEngines()["BTC-000_BNB"].LastMatchHeight = -1 testApp.DexKeeper.FeeManager.UpdateConfig(newTestFeeConfig()) // setup accounts @@ -209,9 +213,11 @@ func Test_Match(t *testing.T) { buys, sells, pendingMatch := getOrderBook("BTC-000_BNB") assert.Equal(4, len(buys)) assert.Equal(3, len(sells)) + assert.Equal(true, pendingMatch) - testApp.DexKeeper.MatchAndAllocateAll(ctx, nil) + testApp.DexKeeper.MatchAndAllocateSymbols(ctx, nil, false) buys, sells, pendingMatch = getOrderBook("BTC-000_BNB") + assert.Equal(0, len(buys)) assert.Equal(3, len(sells)) assert.Equal(false, pendingMatch) @@ -266,8 +272,9 @@ func Test_Match(t *testing.T) { assert.Equal(4, len(buys)) assert.Equal(3, len(sells)) - testApp.DexKeeper.MatchAndAllocateAll(ctx, nil) + testApp.DexKeeper.MatchAndAllocateSymbols(ctx, nil, false) buys, sells, _ = getOrderBook("ETH-000_BNB") + t.Logf("buys: %v", buys) t.Logf("sells: %v", sells) assert.Equal(1, len(buys)) @@ -313,6 +320,7 @@ func Test_handleCancelOrder_CheckTx(t *testing.T) { tradingPair := types.NewTradingPair("BTC-000", "BNB", 1e8) testApp.DexKeeper.PairMapper.AddTradingPair(ctx, tradingPair) testApp.DexKeeper.AddEngine(tradingPair) + testApp.DexKeeper.GetEngines()["BTC-000_BNB"].LastMatchHeight = -1 testApp.DexKeeper.FeeManager.UpdateConfig(newTestFeeConfig()) // setup accounts diff --git a/app/config/config.go b/app/config/config.go index 461f82b6f..12228fb60 100644 --- a/app/config/config.go +++ b/app/config/config.go @@ -67,6 +67,8 @@ LotSizeUpgradeHeight = {{ .UpgradeConfig.LotSizeUpgradeHeight }} ListingRuleUpgradeHeight = {{ .UpgradeConfig.ListingRuleUpgradeHeight }} # Block height of FixZeroBalanceHeight upgrade FixZeroBalanceHeight = {{ .UpgradeConfig.FixZeroBalanceHeight }} +# Block height of BEP8 upgrade +BEP8Height = {{ .UpgradeConfig.BEP8Height }} # Block height of BEP67 upgrade BEP67Height = {{ .UpgradeConfig.BEP67Height }} # Block height of BEP70 upgrade @@ -360,6 +362,7 @@ func defaultBaseConfig() *BaseConfig { } type UpgradeConfig struct { + // Galileo Upgrade BEP6Height int64 `mapstructure:"BEP6Height"` BEP9Height int64 `mapstructure:"BEP9Height"` @@ -368,7 +371,7 @@ type UpgradeConfig struct { // Hubble Upgrade BEP12Height int64 `mapstructure:"BEP12Height"` // Archimedes Upgrade - BEP3Height int64 `mapstructure:"BEP3Height"` + BEP3Height int64 `mapstructure:"BEP3Height"` // TODO: add upgrade name FixSignBytesOverflowHeight int64 `mapstructure:"FixSignBytesOverflowHeight"` @@ -376,6 +379,8 @@ type UpgradeConfig struct { ListingRuleUpgradeHeight int64 `mapstructure:"ListingRuleUpgradeHeight"` FixZeroBalanceHeight int64 `mapstructure:"FixZeroBalanceHeight"` + // TODO: add upgrade name + BEP8Height int64 `mapstructure:"BEP8Height"` BEP67Height int64 `mapstructure:"BEP67Height"` BEP70Height int64 `mapstructure:"BEP70Height"` } @@ -393,8 +398,9 @@ func defaultUpgradeConfig() *UpgradeConfig { LotSizeUpgradeHeight: math.MaxInt64, ListingRuleUpgradeHeight: math.MaxInt64, FixZeroBalanceHeight: math.MaxInt64, - BEP67Height: 1, - BEP70Height: 1, + BEP8Height: math.MaxInt64, + BEP67Height: math.MaxInt64, + BEP70Height: math.MaxInt64, } } diff --git a/app/helpers.go b/app/helpers.go index 84eaa12cd..8908dc815 100644 --- a/app/helpers.go +++ b/app/helpers.go @@ -142,16 +142,12 @@ func (app *BinanceChain) processErrAbciResponseForPub(txBytes []byte) { case order.NewOrderMsg: app.Logger.Info("failed to process NewOrderMsg", "oid", msg.Id) // The error on deliver should be rare and only impact witness publisher's performance - app.DexKeeper.OrderChangesMtx.Lock() - app.DexKeeper.OrderChanges = append(app.DexKeeper.OrderChanges, order.OrderChange{msg.Id, order.FailedBlocking, "", msg}) - app.DexKeeper.OrderChangesMtx.Unlock() + app.DexKeeper.UpdateOrderChangeSync(order.OrderChange{msg.Id, order.FailedBlocking, "", msg}, msg.Symbol) case order.CancelOrderMsg: app.Logger.Info("failed to process CancelOrderMsg", "oid", msg.RefId) // The error on deliver should be rare and only impact witness publisher's performance - app.DexKeeper.OrderChangesMtx.Lock() - // OrderInfo must has been in keeper.OrderInfosForPub - app.DexKeeper.OrderChanges = append(app.DexKeeper.OrderChanges, order.OrderChange{msg.RefId, order.FailedBlocking, "", msg}) - app.DexKeeper.OrderChangesMtx.Unlock() + // OrderInfo must has been in keeper.orderInfosForPub + app.DexKeeper.UpdateOrderChangeSync(order.OrderChange{msg.RefId, order.FailedBlocking, "", msg}, msg.Symbol) default: // deliberately do nothing for message other than NewOrderMsg // in future, we may publish fail status of send msg @@ -193,6 +189,7 @@ func (app *BinanceChain) reInitChain() error { snapshot.Manager().GetStateDB(), app.LastBlockHeight(), app.TxDecoder) + app.initParams() // init app cache diff --git a/app/pub/helpers.go b/app/pub/helpers.go index dfd7ec216..c01dc541c 100644 --- a/app/pub/helpers.go +++ b/app/pub/helpers.go @@ -8,6 +8,8 @@ import ( "sync" "time" + abci "github.com/tendermint/tendermint/abci/types" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/bank" @@ -20,27 +22,28 @@ import ( "github.com/binance-chain/node/plugins/tokens/burn" "github.com/binance-chain/node/plugins/tokens/freeze" "github.com/binance-chain/node/plugins/tokens/issue" - abci "github.com/tendermint/tendermint/abci/types" + "github.com/binance-chain/node/plugins/tokens/seturi" ) -func GetTradeAndOrdersRelatedAccounts(kp *orderPkg.Keeper, tradesToPublish []*Trade) []string { - res := make([]string, 0, len(tradesToPublish)*2+len(kp.OrderChanges)) +func GetTradeAndOrdersRelatedAccounts(tradesToPublish []*Trade, orderChanges orderPkg.OrderChanges, orderInfosForPublish orderPkg.OrderInfoForPublish) []string { + res := make([]string, 0, len(tradesToPublish)*2+len(orderChanges)) for _, t := range tradesToPublish { - if bo, ok := kp.OrderInfosForPub[t.Bid]; ok { + + if bo, ok := orderInfosForPublish[t.Bid]; ok { res = append(res, string(bo.Sender.Bytes())) } else { Logger.Error("failed to locate buy order in OrderChangesMap for trade account resolving", "bid", t.Bid) } - if so, ok := kp.OrderInfosForPub[t.Sid]; ok { + if so, ok := orderInfosForPublish[t.Sid]; ok { res = append(res, string(so.Sender.Bytes())) } else { Logger.Error("failed to locate sell order in OrderChangesMap for trade account resolving", "sid", t.Sid) } } - for _, orderChange := range kp.OrderChanges { - if orderInfo := kp.OrderInfosForPub[orderChange.Id]; orderInfo != nil { + for _, orderChange := range orderChanges { + if orderInfo := orderInfosForPublish[orderChange.Id]; orderInfo != nil { res = append(res, string(orderInfo.Sender.Bytes())) } else { Logger.Error("failed to locate order change in OrderChangesMap", "orderChange", orderChange.String()) @@ -128,6 +131,12 @@ func GetBlockPublished(pool *sdk.Pool, header abci.Header, blockHash []byte) *Bl case freeze.UnfreezeMsg: txAsset = msg.Symbol // will not cover timelock, timeUnlock, timeRelock, atomic Swap + case issue.IssueMiniMsg: + txAsset = msg.Symbol + case issue.IssueTinyMsg: + txAsset = msg.Symbol + case seturi.SetURIMsg: + txAsset = msg.Symbol } transactionsToPublish = append(transactionsToPublish, Transaction{ TxHash: txhash, @@ -259,12 +268,10 @@ func GetAccountBalances(mapper auth.AccountKeeper, ctx sdk.Context, accSlices .. return } -func MatchAndAllocateAllForPublish( - dexKeeper *orderPkg.Keeper, - ctx sdk.Context) []*Trade { - // This channels is used for protect not update `dexKeeper.OrderChanges` concurrently +func MatchAndAllocateAllForPublish(dexKeeper *orderPkg.DexKeeper, ctx sdk.Context, matchAllMiniSymbols bool) []*Trade { + // This channels is used for protect not update `dexKeeper.orderChanges` concurrently // matcher would send item to postAlloTransHandler in several goroutine (well-designed) - // while dexKeeper.OrderChanges are not separated by concurrent factor (users here) + // while dexKeeper.orderChanges are not separated by concurrent factor (users here) iocExpireFeeHolderCh := make(chan orderPkg.ExpireHolder, TransferCollectionChannelSize) wg := sync.WaitGroup{} wg.Add(1) @@ -274,21 +281,27 @@ func MatchAndAllocateAllForPublish( if tran.IsExpire() { if tran.IsExpiredWithFee() { // we only got expire of Ioc here, gte orders expire is handled in breathe block - iocExpireFeeHolderCh <- orderPkg.ExpireHolder{tran.Oid, orderPkg.IocNoFill, tran.Fee.String()} + iocExpireFeeHolderCh <- orderPkg.ExpireHolder{tran.Oid, orderPkg.IocNoFill, tran.Fee.String(), tran.Symbol} } else { - iocExpireFeeHolderCh <- orderPkg.ExpireHolder{tran.Oid, orderPkg.IocExpire, tran.Fee.String()} + iocExpireFeeHolderCh <- orderPkg.ExpireHolder{tran.Oid, orderPkg.IocExpire, tran.Fee.String(), tran.Symbol} } } } - dexKeeper.MatchAndAllocateAll(ctx, postAlloTransHandler) + dexKeeper.MatchAndAllocateSymbols(ctx, postAlloTransHandler, matchAllMiniSymbols) close(iocExpireFeeHolderCh) - tradeIdx := 0 - tradesToPublish := make([]*Trade, 0) tradeHeight := ctx.BlockHeight() - for _, pair := range dexKeeper.PairMapper.ListAllTradingPairs(ctx) { - symbol := pair.GetSymbol() + tradesToPublish := extractTradesToPublish(dexKeeper, tradeHeight) + wg.Wait() + return tradesToPublish +} + +func extractTradesToPublish(dexKeeper *orderPkg.DexKeeper, tradeHeight int64) (tradesToPublish []*Trade) { + tradesToPublish = make([]*Trade, 0, 32) + tradeIdx := 0 + + for symbol := range dexKeeper.GetEngines() { matchEngTrades, _ := dexKeeper.GetLastTrades(tradeHeight, symbol) for _, trade := range matchEngTrades { var ssinglefee string @@ -317,13 +330,11 @@ func MatchAndAllocateAllForPublish( tradesToPublish = append(tradesToPublish, t) } } - - wg.Wait() return tradesToPublish } func ExpireOrdersForPublish( - dexKeeper *orderPkg.Keeper, + dexKeeper *orderPkg.DexKeeper, ctx sdk.Context, blockTime time.Time) { expireHolderCh := make(chan orderPkg.ExpireHolder, TransferCollectionChannelSize) @@ -332,7 +343,7 @@ func ExpireOrdersForPublish( go updateExpireFeeForPublish(dexKeeper, &wg, expireHolderCh) var collectorForExpires = func(tran orderPkg.Transfer) { if tran.IsExpire() { - expireHolderCh <- orderPkg.ExpireHolder{tran.Oid, orderPkg.Expired, tran.Fee.String()} + expireHolderCh <- orderPkg.ExpireHolder{tran.Oid, orderPkg.Expired, tran.Fee.String(), tran.Symbol} } } dexKeeper.ExpireOrders(ctx, blockTime, collectorForExpires) @@ -341,14 +352,14 @@ func ExpireOrdersForPublish( return } -func DelistTradingPairForPublish(ctx sdk.Context, dexKeeper *orderPkg.Keeper, symbol string) { +func DelistTradingPairForPublish(ctx sdk.Context, dexKeeper *orderPkg.DexKeeper, symbol string) { expireHolderCh := make(chan orderPkg.ExpireHolder, TransferCollectionChannelSize) wg := sync.WaitGroup{} wg.Add(1) go updateExpireFeeForPublish(dexKeeper, &wg, expireHolderCh) var collectorForExpires = func(tran orderPkg.Transfer) { if tran.IsExpire() { - expireHolderCh <- orderPkg.ExpireHolder{tran.Oid, orderPkg.Expired, tran.Fee.String()} + expireHolderCh <- orderPkg.ExpireHolder{tran.Oid, orderPkg.Expired, tran.Fee.String(), tran.Symbol} } } dexKeeper.DelistTradingPair(ctx, symbol, collectorForExpires) @@ -380,14 +391,14 @@ func CollectStakeUpdatesForPublish(unbondingDelegations []stake.UnbondingDelegat } func updateExpireFeeForPublish( - dexKeeper *orderPkg.Keeper, + dexKeeper *orderPkg.DexKeeper, wg *sync.WaitGroup, expHolderCh <-chan orderPkg.ExpireHolder) { defer wg.Done() for expHolder := range expHolderCh { Logger.Debug("transfer collector for order", "orderId", expHolder.OrderId) change := orderPkg.OrderChange{expHolder.OrderId, expHolder.Reason, expHolder.Fee, nil} - dexKeeper.OrderChanges = append(dexKeeper.OrderChanges, change) + dexKeeper.UpdateOrderChangeSync(change, expHolder.Symbol) } } @@ -507,8 +518,7 @@ func collectOrdersToPublish( orderInfos orderPkg.OrderInfoForPublish, feeHolder orderPkg.FeeHolder, timestamp int64) (opensToPublish []*Order, closedToPublish []*Order, feeToPublish map[string]string) { - opensToPublish = make([]*Order, 0) - closedToPublish = make([]*Order, 0) + // serve as a cache to avoid fee's serialization several times for one address feeToPublish = make(map[string]string) @@ -519,43 +529,45 @@ func collectOrdersToPublish( chargedExpires := make(map[string]int) // collect orders (new, cancel, ioc-no-fill, expire, failed-blocking and failed-matching) from orderChanges - for _, o := range orderChanges { - if orderInfo := o.ResolveOrderInfo(orderInfos); orderInfo != nil { - orderToPublish := Order{ - orderInfo.Symbol, o.Tpe, o.Id, - "", orderInfo.Sender.String(), orderInfo.Side, - orderPkg.OrderType.LIMIT, orderInfo.Price, orderInfo.Quantity, - 0, 0, orderInfo.CumQty, "", - orderInfo.CreatedTimestamp, timestamp, orderInfo.TimeInForce, - orderPkg.NEW, orderInfo.TxHash, o.SingleFee, - } + opensToPublish, closedToPublish = collectOrders(orderChanges, orderInfos, timestamp, chargedCancels, chargedExpires) - if o.Tpe.IsOpen() { + // update C and E fields in serialized fee string + updateCancelExpireOrderNumInFees(closedToPublish, orderInfos, feeToPublish, chargedCancels, chargedExpires, feeHolder) + + // update fee and collect orders from trades + opensToPublish, closedToPublish = convertTradesToOrders(trades, orderInfos, timestamp, feeHolder, feeToPublish, opensToPublish, closedToPublish) + + return opensToPublish, closedToPublish, feeToPublish +} + +func convertTradesToOrders(trades []*Trade, orderInfos orderPkg.OrderInfoForPublish, timestamp int64, feeHolder orderPkg.FeeHolder, feeToPublish map[string]string, opensToPublish []*Order, closedToPublish []*Order) ([]*Order, []*Order) { + for _, t := range trades { + if o, exists := orderInfos[t.Bid]; exists { + orderToPublish := tradeToOrder(t, o, timestamp, feeHolder, feeToPublish) + if orderToPublish.Status.IsOpen() { opensToPublish = append(opensToPublish, &orderToPublish) } else { closedToPublish = append(closedToPublish, &orderToPublish) } + } else { + Logger.Error("failed to resolve order information from orderInfos", "orderId", t.Bid) + } - // fee field handling - if orderToPublish.isChargedCancel() { - if _, ok := chargedCancels[string(orderInfo.Sender)]; ok { - chargedCancels[string(orderInfo.Sender)] += 1 - } else { - chargedCancels[string(orderInfo.Sender)] = 1 - } - } else if orderToPublish.isChargedExpire() { - if _, ok := chargedExpires[string(orderInfo.Sender)]; ok { - chargedExpires[string(orderInfo.Sender)] += 1 - } else { - chargedExpires[string(orderInfo.Sender)] = 1 - } + if o, exists := orderInfos[t.Sid]; exists { + orderToPublish := tradeToOrder(t, o, timestamp, feeHolder, feeToPublish) + if orderToPublish.Status.IsOpen() { + opensToPublish = append(opensToPublish, &orderToPublish) + } else { + closedToPublish = append(closedToPublish, &orderToPublish) } } else { - Logger.Error("failed to locate order change in OrderChangesMap", "orderChange", o.String()) + Logger.Error("failed to resolve order information from orderInfos", "orderId", t.Sid) } } + return opensToPublish, closedToPublish +} - // update C and E fields in serialized fee string +func updateCancelExpireOrderNumInFees(closedToPublish []*Order, orderInfos orderPkg.OrderInfoForPublish, feeToPublish map[string]string, chargedCancels map[string]int, chargedExpires map[string]int, feeHolder orderPkg.FeeHolder) { for _, order := range closedToPublish { if orderInfo, ok := orderInfos[order.OrderId]; ok { senderBytesStr := string(orderInfo.Sender) @@ -574,33 +586,47 @@ func collectOrdersToPublish( Logger.Error("should not to locate order in OrderChangesMap", "oid", order.OrderId) } } +} - // update fee and collect orders from trades - for _, t := range trades { - if o, exists := orderInfos[t.Bid]; exists { - orderToPublish := tradeToOrder(t, o, timestamp, feeHolder, feeToPublish) - if orderToPublish.Status.IsOpen() { - opensToPublish = append(opensToPublish, &orderToPublish) - } else { - closedToPublish = append(closedToPublish, &orderToPublish) +func collectOrders(orderChanges orderPkg.OrderChanges, orderInfos orderPkg.OrderInfoForPublish, timestamp int64, chargedCancels map[string]int, chargedExpires map[string]int) ([]*Order, []*Order) { + opensToPublish := make([]*Order, 0) + closedToPublish := make([]*Order, 0) + for _, o := range orderChanges { + if orderInfo := o.ResolveOrderInfo(orderInfos); orderInfo != nil { + orderToPublish := Order{ + orderInfo.Symbol, o.Tpe, o.Id, + "", orderInfo.Sender.String(), orderInfo.Side, + orderPkg.OrderType.LIMIT, orderInfo.Price, orderInfo.Quantity, + 0, 0, orderInfo.CumQty, "", + orderInfo.CreatedTimestamp, timestamp, orderInfo.TimeInForce, + orderPkg.NEW, orderInfo.TxHash, o.SingleFee, } - } else { - Logger.Error("failed to resolve order information from orderInfos", "orderId", t.Bid) - } - if o, exists := orderInfos[t.Sid]; exists { - orderToPublish := tradeToOrder(t, o, timestamp, feeHolder, feeToPublish) - if orderToPublish.Status.IsOpen() { + if o.Tpe.IsOpen() { opensToPublish = append(opensToPublish, &orderToPublish) } else { closedToPublish = append(closedToPublish, &orderToPublish) } + + // fee field handling + if orderToPublish.isChargedCancel() { + if _, ok := chargedCancels[string(orderInfo.Sender)]; ok { + chargedCancels[string(orderInfo.Sender)] += 1 + } else { + chargedCancels[string(orderInfo.Sender)] = 1 + } + } else if orderToPublish.isChargedExpire() { + if _, ok := chargedExpires[string(orderInfo.Sender)]; ok { + chargedExpires[string(orderInfo.Sender)] += 1 + } else { + chargedExpires[string(orderInfo.Sender)] = 1 + } + } } else { - Logger.Error("failed to resolve order information from orderInfos", "orderId", t.Sid) + Logger.Error("failed to locate order change in OrderChangesMap", "orderChange", o.String()) } } - - return opensToPublish, closedToPublish, feeToPublish + return opensToPublish, closedToPublish } func getSerializedFeeForOrder(orderInfo *orderPkg.OrderInfo, status orderPkg.ChangeType, feeHolder orderPkg.FeeHolder, feeToPublish map[string]string) string { diff --git a/app/pub/keeper_pub_test.go b/app/pub/keeper_pub_test.go index 77cec88e3..a3c37b0e7 100644 --- a/app/pub/keeper_pub_test.go +++ b/app/pub/keeper_pub_test.go @@ -39,7 +39,7 @@ func newTestFeeConfig() orderPkg.FeeConfig { return feeConfig } -var keeper *orderPkg.Keeper +var keeper *orderPkg.DexKeeper var buyer sdk.AccAddress var seller sdk.AccAddress var am auth.AccountKeeper @@ -61,7 +61,7 @@ func setupKeeperTest(t *testing.T) (*assert.Assertions, *require.Assertions) { ctx = sdk.NewContext(ms, abci.Header{ChainID: "mychainid"}, sdk.RunTxModeDeliver, logger).WithAccountCache(accountCache) pairMapper := store.NewTradingPairMapper(cdc, common.PairStoreKey) - keeper = orderPkg.NewKeeper(capKey2, am, pairMapper, sdk.NewCodespacer().RegisterNext(dextypes.DefaultCodespace), 2, cdc, true) + keeper = orderPkg.NewDexKeeper(capKey2, am, pairMapper, sdk.NewCodespacer().RegisterNext(dextypes.DefaultCodespace), 2, cdc, true) tradingPair := dextypes.NewTradingPair("XYZ-000", types.NativeTokenSymbol, 1e8) keeper.PairMapper.AddTradingPair(ctx, tradingPair) keeper.AddEngine(tradingPair) @@ -72,10 +72,10 @@ func setupKeeperTest(t *testing.T) (*assert.Assertions, *require.Assertions) { keeper.FeeConfig.SetFeeRate(ctx, 1000) keeper.FeeConfig.SetFeeRateNative(ctx, 500) - _, buyerAcc := testutils.NewAccountForPub(ctx, am, 100000000000, 100000000000, 100000000000) // give user enough coins to pay the fee + _, buyerAcc := testutils.NewAccountForPub(ctx, am, 100000000000, 100000000000, 100000000000, "XYZ-000") // give user enough coins to pay the fee buyer = buyerAcc.GetAddress() - _, sellerAcc := testutils.NewAccountForPub(ctx, am, 100000000000, 100000000000, 100000000000) + _, sellerAcc := testutils.NewAccountForPub(ctx, am, 100000000000, 100000000000, 100000000000, "XYZ-000") seller = sellerAcc.GetAddress() return assert.New(t), require.New(t) @@ -89,15 +89,15 @@ func TestKeeper_AddOrder(t *testing.T) { msg = orderPkg.NewNewOrderMsg(buyer, "2", orderPkg.Side.BUY, "XYZ-000_BNB", 101000, 1000000) keeper.AddOrder(orderPkg.OrderInfo{msg, 43, 105, 43, 105, 0, "0D42245EB2BF574A5B9D485404E0E61B1A2397A9", 0}, false) - require.Len(keeper.OrderChanges, 2) - require.Len(keeper.OrderInfosForPub, 2) + require.Len(keeper.GetOrderChanges(orderPkg.PairType.BEP2), 2) + require.Len(keeper.GetOrderInfosForPub(orderPkg.PairType.BEP2), 2) // verify order0 - and the order in orderchanges slice - orderChange0 := keeper.OrderChanges[0] + orderChange0 := keeper.GetOrderChanges(orderPkg.PairType.BEP2)[0] assert.Equal("1", orderChange0.Id) assert.Equal(orderPkg.Ack, orderChange0.Tpe) // verify order1 - make sure the fields are correct - orderInfo1 := keeper.OrderInfosForPub["2"] + orderInfo1 := keeper.GetOrderInfosForPub(orderPkg.PairType.BEP2)["2"] assert.Equal(buyer, orderInfo1.Sender) assert.Equal("2", orderInfo1.Id) assert.Equal("XYZ-000_BNB", orderInfo1.Symbol) @@ -117,17 +117,19 @@ func TestKeeper_IOCExpireWithFee(t *testing.T) { msg := orderPkg.NewOrderMsg{buyer, "1", "XYZ-000_BNB", orderPkg.OrderType.LIMIT, orderPkg.Side.BUY, 102000, 3000000, orderPkg.TimeInForce.IOC} keeper.AddOrder(orderPkg.OrderInfo{msg, 42, 100, 42, 100, 0, "08E19B16880CF70D59DDD996E3D75C66CD0405DE", 0}, false) - require.Len(keeper.OrderChanges, 1) - require.Len(keeper.OrderInfosForPub, 1) + require.Len(keeper.GetOrderChanges(orderPkg.PairType.BEP2), 1) + require.Len(keeper.GetOrderInfosForPub(orderPkg.PairType.BEP2), 1) - trades := MatchAndAllocateAllForPublish(keeper, ctx) + trades := MatchAndAllocateAllForPublish(keeper, ctx, false) - require.Len(keeper.OrderChanges, 2) - require.Len(keeper.OrderInfosForPub, 1) + require.Len(keeper.GetOrderChanges(orderPkg.PairType.BEP2), 2) + require.Len(keeper.GetOrderInfosForPub(orderPkg.PairType.BEP2), 1) require.Len(trades, 0) + require.Len(keeper.GetAllOrderChanges(), 2) + require.Len(keeper.GetAllOrderInfosForPub(), 1) - orderChange0 := keeper.OrderChanges[0] - orderChange1 := keeper.OrderChanges[1] + orderChange0 := keeper.GetOrderChanges(orderPkg.PairType.BEP2)[0] + orderChange1 := keeper.GetOrderChanges(orderPkg.PairType.BEP2)[1] // verify orderChange0 - Ack assert.Equal("1", orderChange0.Id) assert.Equal(orderPkg.Ack, orderChange0.Tpe) @@ -143,17 +145,17 @@ func TestKeeper_ExpireWithFee(t *testing.T) { msg := orderPkg.NewOrderMsg{buyer, "1", "XYZ-000_BNB", orderPkg.OrderType.LIMIT, orderPkg.Side.BUY, 102000, 3000000, orderPkg.TimeInForce.GTE} keeper.AddOrder(orderPkg.OrderInfo{msg, 42, 100, 42, 100, 0, "08E19B16880CF70D59DDD996E3D75C66CD0405DE", 0}, false) - require.Len(keeper.OrderChanges, 1) - require.Len(keeper.OrderInfosForPub, 1) + require.Len(keeper.GetOrderChanges(orderPkg.PairType.BEP2), 1) + require.Len(keeper.GetOrderInfosForPub(orderPkg.PairType.BEP2), 1) breathTime := prepareExpire(int64(43)) ExpireOrdersForPublish(keeper, am, ctx, breathTime) - require.Len(keeper.OrderChanges, 2) - require.Len(keeper.OrderInfosForPub, 1) + require.Len(keeper.GetOrderChanges(orderPkg.PairType.BEP2), 2) + require.Len(keeper.GetOrderInfosForPub(orderPkg.PairType.BEP2), 1) - orderChange0 := keeper.OrderChanges[0] - orderChange1 := keeper.OrderChanges[1] + orderChange0 := keeper.GetOrderChanges(orderPkg.PairType.BEP2)[0] + orderChange1 := keeper.GetOrderChanges(orderPkg.PairType.BEP2)[1] // verify orderChange0 - Ack assert.Equal("1", orderChange0.Id) assert.Equal(orderPkg.Ack, orderChange0.Tpe) @@ -168,16 +170,16 @@ func TestKeeper_DelistWithFee(t *testing.T) { msg := orderPkg.NewOrderMsg{buyer, "1", "XYZ-000_BNB", orderPkg.OrderType.LIMIT, orderPkg.Side.BUY, 102000, 3000000, orderPkg.TimeInForce.GTE} keeper.AddOrder(orderPkg.OrderInfo{msg, 42, 100, 42, 100, 0, "08E19B16880CF70D59DDD996E3D75C66CD0405DE", 0}, false) - require.Len(keeper.OrderChanges, 1) - require.Len(keeper.OrderInfosForPub, 1) + require.Len(keeper.GetOrderChanges(orderPkg.PairType.BEP2), 1) + require.Len(keeper.GetOrderInfosForPub(orderPkg.PairType.BEP2), 1) DelistTradingPairForPublish(ctx, keeper, "XYZ-000_BNB") - require.Len(keeper.OrderChanges, 2) - require.Len(keeper.OrderInfosForPub, 1) + require.Len(keeper.GetOrderChanges(orderPkg.PairType.BEP2), 2) + require.Len(keeper.GetOrderInfosForPub(orderPkg.PairType.BEP2), 1) - orderChange0 := keeper.OrderChanges[0] - orderChange1 := keeper.OrderChanges[1] + orderChange0 := keeper.GetOrderChanges(orderPkg.PairType.BEP2)[0] + orderChange1 := keeper.GetOrderChanges(orderPkg.PairType.BEP2)[1] // verify orderChange0 - Ack assert.Equal("1", orderChange0.Id) @@ -195,10 +197,10 @@ func Test_IOCPartialExpire(t *testing.T) { msg2 := orderPkg.NewOrderMsg{seller, "s-1", "XYZ-000_BNB", orderPkg.OrderType.LIMIT, orderPkg.Side.SELL, 100000000, 100000000, orderPkg.TimeInForce.GTE} keeper.AddOrder(orderPkg.OrderInfo{msg2, 42, 100, 42, 100, 0, "", 0}, false) - require.Len(keeper.OrderChanges, 2) - require.Len(keeper.OrderInfosForPub, 2) - orderChange0 := keeper.OrderChanges[0] - orderChange1 := keeper.OrderChanges[1] + require.Len(keeper.GetOrderChanges(orderPkg.PairType.BEP2), 2) + require.Len(keeper.GetOrderInfosForPub(orderPkg.PairType.BEP2), 2) + orderChange0 := keeper.GetOrderChanges(orderPkg.PairType.BEP2)[0] + orderChange1 := keeper.GetOrderChanges(orderPkg.PairType.BEP2)[1] // verify orderChange0 - Ack assert.Equal("b-1", orderChange0.Id) assert.Equal(orderPkg.Ack, orderChange0.Tpe) @@ -206,10 +208,10 @@ func Test_IOCPartialExpire(t *testing.T) { assert.Equal("s-1", orderChange1.Id) assert.Equal(orderPkg.Ack, orderChange1.Tpe) - trades := MatchAndAllocateAllForPublish(keeper, ctx) + trades := MatchAndAllocateAllForPublish(keeper, ctx, false) - require.Len(keeper.OrderChanges, 3) - require.Len(keeper.OrderInfosForPub, 2) + require.Len(keeper.GetOrderChanges(orderPkg.PairType.BEP2), 3) + require.Len(keeper.GetOrderInfosForPub(orderPkg.PairType.BEP2), 2) require.Len(trades, 1) trade0 := trades[0] assert.Equal("0-0", trade0.Id) @@ -219,7 +221,7 @@ func Test_IOCPartialExpire(t *testing.T) { assert.Equal("s-1", trade0.Sid) assert.Equal("b-1", trade0.Bid) - orderChange2 := keeper.OrderChanges[2] + orderChange2 := keeper.GetOrderChanges(orderPkg.PairType.BEP2)[2] assert.Equal("b-1", orderChange2.Id) assert.Equal(orderPkg.IocExpire, orderChange2.Tpe) @@ -235,10 +237,10 @@ func Test_GTEPartialExpire(t *testing.T) { msg2 := orderPkg.NewOrderMsg{seller, "s-1", "XYZ-000_BNB", orderPkg.OrderType.LIMIT, orderPkg.Side.SELL, 100000000, 300000000, orderPkg.TimeInForce.GTE} keeper.AddOrder(orderPkg.OrderInfo{msg2, 42, 100, 42, 100, 0, "", 0}, false) - require.Len(keeper.OrderChanges, 2) - require.Len(keeper.OrderInfosForPub, 2) - orderChange0 := keeper.OrderChanges[0] - orderChange1 := keeper.OrderChanges[1] + require.Len(keeper.GetOrderChanges(orderPkg.PairType.BEP2), 2) + require.Len(keeper.GetOrderInfosForPub(orderPkg.PairType.BEP2), 2) + orderChange0 := keeper.GetOrderChanges(orderPkg.PairType.BEP2)[0] + orderChange1 := keeper.GetOrderChanges(orderPkg.PairType.BEP2)[1] // verify orderChange0 - Ack assert.Equal("b-1", orderChange0.Id) assert.Equal(orderPkg.Ack, orderChange0.Tpe) @@ -246,7 +248,7 @@ func Test_GTEPartialExpire(t *testing.T) { assert.Equal("s-1", orderChange1.Id) assert.Equal(orderPkg.Ack, orderChange1.Tpe) - trades := MatchAndAllocateAllForPublish(keeper, ctx) + trades := MatchAndAllocateAllForPublish(keeper, ctx, false) require.Len(trades, 1) trade0 := trades[0] assert.Equal("0-0", trade0.Id) @@ -259,15 +261,15 @@ func Test_GTEPartialExpire(t *testing.T) { assert.Equal("BNB:50000", keeper.RoundOrderFees[string(buyer.Bytes())].String()) assert.Equal("BNB:50000", keeper.RoundOrderFees[string(seller.Bytes())].String()) - require.Len(keeper.OrderChanges, 2) // for GTE order, fully fill is not derived from transfer (will be generated by trade) - require.Len(keeper.OrderInfosForPub, 2) + require.Len(keeper.GetOrderChanges(orderPkg.PairType.BEP2), 2) // for GTE order, fully fill is not derived from transfer (will be generated by trade) + require.Len(keeper.GetOrderInfosForPub(orderPkg.PairType.BEP2), 2) // let the sell order expire breathTime := prepareExpire(int64(43)) ExpireOrdersForPublish(keeper, am, ctx, breathTime) - require.Len(keeper.OrderChanges, 3) - orderChange2 := keeper.OrderChanges[2] + require.Len(keeper.GetOrderChanges(orderPkg.PairType.BEP2), 3) + orderChange2 := keeper.GetOrderChanges(orderPkg.PairType.BEP2)[2] assert.Equal("s-1", orderChange2.Id) assert.Equal(orderPkg.Expired, orderChange2.Tpe) } @@ -282,11 +284,11 @@ func Test_OneBuyVsTwoSell(t *testing.T) { msg3 := orderPkg.NewOrderMsg{seller, "s-2", "XYZ-000_BNB", orderPkg.OrderType.LIMIT, orderPkg.Side.SELL, 100000000, 200000000, orderPkg.TimeInForce.GTE} keeper.AddOrder(orderPkg.OrderInfo{msg3, 42, 100, 42, 100, 0, "", 0}, false) - require.Len(keeper.OrderChanges, 3) - require.Len(keeper.OrderInfosForPub, 3) - orderChange0 := keeper.OrderChanges[0] - orderChange1 := keeper.OrderChanges[1] - orderChange2 := keeper.OrderChanges[2] + require.Len(keeper.GetOrderChanges(orderPkg.PairType.BEP2), 3) + require.Len(keeper.GetOrderInfosForPub(orderPkg.PairType.BEP2), 3) + orderChange0 := keeper.GetOrderChanges(orderPkg.PairType.BEP2)[0] + orderChange1 := keeper.GetOrderChanges(orderPkg.PairType.BEP2)[1] + orderChange2 := keeper.GetOrderChanges(orderPkg.PairType.BEP2)[2] // verify orderChange0 - Ack assert.Equal("b-1", orderChange0.Id) assert.Equal(orderPkg.Ack, orderChange0.Tpe) @@ -297,7 +299,7 @@ func Test_OneBuyVsTwoSell(t *testing.T) { assert.Equal("s-2", orderChange2.Id) assert.Equal(orderPkg.Ack, orderChange2.Tpe) - trades := MatchAndAllocateAllForPublish(keeper, ctx) + trades := MatchAndAllocateAllForPublish(keeper, ctx, false) require.Len(trades, 2) trade0 := trades[0] assert.Equal("0-0", trade0.Id) diff --git a/app/pub/msgs.go b/app/pub/msgs.go index bf55031fb..b97ca0897 100644 --- a/app/pub/msgs.go +++ b/app/pub/msgs.go @@ -121,12 +121,14 @@ func (msg *ExecutionResults) ToNativeMap() map[string]interface{} { if msg.StakeUpdates.NumOfMsgs > 0 { native["stakeUpdates"] = map[string]interface{}{"org.binance.dex.model.avro.StakeUpdates": msg.StakeUpdates.ToNativeMap()} } + return native } func (msg *ExecutionResults) EssentialMsg() string { // mainly used to recover for large breathe block expiring message, there should be no trade on breathe block orders := msg.Orders.EssentialMsg() + //TODO output other fields: trades, stakeUpdate etc. return fmt.Sprintf("height:%d\ntime:%d\norders:\n%s\n", msg.Height, msg.Timestamp, orders) } diff --git a/app/pub/publisher.go b/app/pub/publisher.go index 33c6d0e6a..eb17b44c4 100644 --- a/app/pub/publisher.go +++ b/app/pub/publisher.go @@ -18,11 +18,16 @@ const ( MaxOrderBookLevel = 100 ) +type OrderSymbolId struct { + Symbol string + Id string +} + var ( Logger tmlog.Logger Cfg *config.PublicationConfig ToPublishCh chan BlockInfoToPublish - ToRemoveOrderIdCh chan string // order ids to remove from keeper.OrderInfoForPublish + ToRemoveOrderIdCh chan OrderSymbolId // order symbol and ids to remove from keeper.OrderInfoForPublish IsLive bool ) @@ -58,14 +63,7 @@ func Publish( marketData.orderInfos, marketData.feeHolder, marketData.timestamp) - for _, o := range closedToPublish { - if ToRemoveOrderIdCh != nil { - Logger.Debug( - "going to delete order from order changes map", - "orderId", o.OrderId, "status", o.Status) - ToRemoveOrderIdCh <- o.OrderId - } - } + addClosedOrder(closedToPublish, ToRemoveOrderIdCh) } // ToRemoveOrderIdCh would be only used in production code @@ -75,6 +73,7 @@ func Publish( } ordersToPublish := append(opensToPublish, closedToPublish...) + if cfg.PublishOrderUpdates { duration := Timer(Logger, "publish all orders", func() { publishExecutionResult( @@ -106,7 +105,7 @@ func Publish( } if cfg.PublishOrderBook { - var changedPrices orderPkg.ChangedPriceLevelsMap + var changedPrices = make(orderPkg.ChangedPriceLevelsMap) duration := Timer(Logger, "prepare order books to publish", func() { changedPrices = filterChangedOrderBooksByOrders(ordersToPublish, marketData.latestPricesLevels) }) @@ -172,6 +171,17 @@ func Publish( } } +func addClosedOrder(closedToPublish []*Order, toRemoveOrderIdCh chan OrderSymbolId) { + if toRemoveOrderIdCh != nil { + for _, o := range closedToPublish { + Logger.Debug( + "going to delete order from order changes map", + "orderId", o.OrderId, "status", o.Status) + toRemoveOrderIdCh <- OrderSymbolId{o.Symbol, o.OrderId} + } + } +} + func Stop(publisher MarketDataPublisher) { if IsLive == false { Logger.Error("publication module has already been stopped") diff --git a/app/pub/types.go b/app/pub/types.go index cec82935e..562162836 100644 --- a/app/pub/types.go +++ b/app/pub/types.go @@ -46,5 +46,6 @@ func NewBlockInfoToPublish( blockFee, feeHolder, transfers, - block} + block, + } } diff --git a/app_test/abci_open_orders_test.go b/app_test/abci_open_orders_test.go index 1b94ad65b..2c5ef262e 100644 --- a/app_test/abci_open_orders_test.go +++ b/app_test/abci_open_orders_test.go @@ -18,8 +18,21 @@ import ( ) func Test_Success(t *testing.T) { - assert, require, pair := setup(t) + baseSymbol := "XYZ-000" + runTestSuccess(t, baseSymbol, true) +} + +func Test_Success_Before_Upgrade(t *testing.T) { + baseSymbol := "XYZ-000" + runTestSuccess(t, baseSymbol, false) +} +func Test_Success_Mini(t *testing.T) { + baseSymbol := "XYZ-000M" + runTestSuccess(t, baseSymbol, true) +} +func runTestSuccess(t *testing.T, symbol string, upgrade bool) { + assert, require, pair := setup(t, symbol, true) msg := orderPkg.NewNewOrderMsg(buyer, "b-1", orderPkg.Side.BUY, pair, 102000, 3000000) keeper.AddOrder(orderPkg.OrderInfo{msg, 100, 0, 100, 0, 0, "", 0}, false) @@ -38,7 +51,7 @@ func Test_Success(t *testing.T) { ctx = ctx.WithBlockHeader(abci.Header{Height: 101, Time: time.Unix(1, 0)}) ctx = ctx.WithBlockHeight(101) - keeper.MatchAndAllocateAll(ctx, nil) + keeper.MatchAndAllocateSymbols(ctx, nil, false) openOrders = issueMustSuccessQuery(pair, buyer, assert) require.Len(openOrders, 1) @@ -59,15 +72,52 @@ func Test_Success(t *testing.T) { } func Test_InvalidPair(t *testing.T) { - assert, _, _ := setup(t) + setChainVersion() + defer resetChainVersion() + baseSymbol := "XYZ-000" + runInvalidPair(t, baseSymbol) +} + +func Test_InvalidPair_Before_Upgrade(t *testing.T) { + baseSymbol := "XYZ-000" + runInvalidPair(t, baseSymbol) +} +func Test_InvalidPair_Mini(t *testing.T) { + setChainVersion() + defer resetChainVersion() + baseSymbol := "XYZ-000M" + runInvalidPair(t, baseSymbol) +} + +func runInvalidPair(t *testing.T, symbol string) { + assert, _, _ := setup(t, symbol, true) res := issueQuery("%afuiewf%@^&2blf", buyer.String()) assert.Equal(uint32(sdk.CodeInternal), res.Code) assert.Equal("pair is not valid", res.Log) } func Test_NonListedPair(t *testing.T) { - assert, _, _ := setup(t) + setChainVersion() + defer resetChainVersion() + baseSymbol := "XYZ-000" + runNonListedPair(t, baseSymbol) +} + +func Test_NonListedPair_Before_Upgrade(t *testing.T) { + baseSymbol := "XYZ-000" + runNonListedPair(t, baseSymbol) +} + +func Test_NonListedPair_Mini(t *testing.T) { + setChainVersion() + defer resetChainVersion() + baseSymbol := "XYZ-000M" + runNonListedPair(t, baseSymbol) +} + +func runNonListedPair(t *testing.T, symbol string) { + assert, _, _ := setup(t, symbol, true) res := issueQuery("NNB-000_BNB", buyer.String()) assert.Equal(uint32(sdk.CodeInternal), res.Code) @@ -75,7 +125,26 @@ func Test_NonListedPair(t *testing.T) { } func Test_InvalidAddr(t *testing.T) { - assert, _, pair := setup(t) + setChainVersion() + defer resetChainVersion() + baseSymbol := "XYZ-000" + runInvalidAddr(t, baseSymbol) +} + +func Test_InvalidAddr_Before_Upgrade(t *testing.T) { + baseSymbol := "XYZ-000" + runInvalidAddr(t, baseSymbol) +} + +func Test_InvalidAddr_Mini(t *testing.T) { + setChainVersion() + defer resetChainVersion() + baseSymbol := "XYZ-000M" + runInvalidAddr(t, baseSymbol) +} + +func runInvalidAddr(t *testing.T, symbol string) { + assert, _, pair := setup(t, symbol, true) res := issueQuery(pair, "%afuiewf%@^&2blf") assert.Equal(uint32(sdk.CodeInternal), res.Code) @@ -83,7 +152,26 @@ func Test_InvalidAddr(t *testing.T) { } func Test_NonExistAddr(t *testing.T) { - assert, _, pair := setup(t) + setChainVersion() + defer resetChainVersion() + baseSymbol := "XYZ-000" + runNonExistAddr(t, baseSymbol) +} + +func Test_NonExistAddr_Before_Upgrade(t *testing.T) { + baseSymbol := "XYZ-000" + runNonExistAddr(t, baseSymbol) +} + +func Test_NonExistAddr_Mini(t *testing.T) { + setChainVersion() + defer resetChainVersion() + baseSymbol := "XYZ-000M" + runNonExistAddr(t, baseSymbol) +} + +func runNonExistAddr(t *testing.T, symbol string) { + assert, _, pair := setup(t, symbol, true) msg := orderPkg.NewNewOrderMsg(seller, "s-1", orderPkg.Side.SELL, pair, 102000, 3000000) keeper.AddOrder(orderPkg.OrderInfo{msg, 100, 0, 100, 0, 0, "", 0}, false) @@ -101,7 +189,7 @@ func issueMustSuccessQuery(pair string, address sdk.AccAddress, assert *assert.A } func issueQuery(pair string, address string) abci.ResponseQuery { - path := fmt.Sprintf("/%s/openorders/%s/%s", dex.AbciQueryPrefix, pair, address) + path := fmt.Sprintf("/%s/openorders/%s/%s", dex.DexAbciQueryPrefix, pair, address) query := abci.RequestQuery{ Path: path, Data: []byte(""), diff --git a/app_test/utils_test.go b/app_test/utils_test.go index 2af12b401..980b81d34 100644 --- a/app_test/utils_test.go +++ b/app_test/utils_test.go @@ -17,6 +17,7 @@ import ( appPkg "github.com/binance-chain/node/app" "github.com/binance-chain/node/common/testutils" "github.com/binance-chain/node/common/types" + "github.com/binance-chain/node/common/upgrade" orderPkg "github.com/binance-chain/node/plugins/dex/order" dextypes "github.com/binance-chain/node/plugins/dex/types" "github.com/binance-chain/node/wire" @@ -24,7 +25,7 @@ import ( // this file has to named with suffix _test, this is a golang bug: https://github.com/golang/go/issues/24895 var ( - keeper *orderPkg.Keeper + keeper *orderPkg.DexKeeper buyer sdk.AccAddress seller sdk.AccAddress am auth.AccountKeeper @@ -33,8 +34,8 @@ var ( cdc *wire.Codec ) -func setup(t *testing.T) (ass *assert.Assertions, req *require.Assertions, pair string) { - baseAssetSymbol := "XYZ-000" +func setup(t *testing.T, symbol string, upgrade bool) (ass *assert.Assertions, req *require.Assertions, pair string) { + baseAssetSymbol := symbol logger := log.NewTMLogger(os.Stdout) db := dbm.NewMemDB() @@ -43,19 +44,32 @@ func setup(t *testing.T) (ass *assert.Assertions, req *require.Assertions, pair ctx = app.GetContextForCheckState() cdc = app.GetCodec() + if upgrade { + setChainVersion() + } + keeper = app.(*appPkg.BinanceChain).DexKeeper tradingPair := dextypes.NewTradingPair(baseAssetSymbol, types.NativeTokenSymbol, 1e8) keeper.PairMapper.AddTradingPair(ctx, tradingPair) keeper.AddEngine(tradingPair) am = app.(*appPkg.BinanceChain).AccountKeeper - _, buyerAcc := testutils.NewAccountForPub(ctx, am, 100000000000, 100000000000, 100000000000) // give user enough coins to pay the fee + _, buyerAcc := testutils.NewAccountForPub(ctx, am, 100000000000, 100000000000, 100000000000, symbol) // give user enough coins to pay the fee buyer = buyerAcc.GetAddress() - _, sellerAcc := testutils.NewAccountForPub(ctx, am, 100000000000, 100000000000, 100000000000) + _, sellerAcc := testutils.NewAccountForPub(ctx, am, 100000000000, 100000000000, 100000000000, symbol) seller = sellerAcc.GetAddress() pair = fmt.Sprintf("%s_%s", baseAssetSymbol, types.NativeTokenSymbol) return assert.New(t), require.New(t), pair } + +func setChainVersion() { + upgrade.Mgr.AddUpgradeHeight(upgrade.BEP8, -1) + upgrade.Mgr.AddUpgradeHeight(upgrade.BEP70, -1) +} + +func resetChainVersion() { + upgrade.Mgr.Config.HeightMap = nil +} diff --git a/common/testutils/testutils.go b/common/testutils/testutils.go index 75587ecc5..c85255808 100644 --- a/common/testutils/testutils.go +++ b/common/testutils/testutils.go @@ -13,20 +13,27 @@ import ( ) func SetupMultiStoreForUnitTest() (sdk.MultiStore, *sdk.KVStoreKey, *sdk.KVStoreKey) { - _, ms, capKey, capKey2 := SetupMultiStoreWithDBForUnitTest() + _, ms, capKey, capKey2, _ := SetupMultiStoreWithDBForUnitTest() return ms, capKey, capKey2 } -func SetupMultiStoreWithDBForUnitTest() (dbm.DB, sdk.MultiStore, *sdk.KVStoreKey, *sdk.KVStoreKey) { +func SetupThreeMultiStoreForUnitTest() (sdk.MultiStore, *sdk.KVStoreKey, *sdk.KVStoreKey, *sdk.KVStoreKey) { + _, ms, capKey, capKey2, capKey3 := SetupMultiStoreWithDBForUnitTest() + return ms, capKey, capKey2, capKey3 +} + +func SetupMultiStoreWithDBForUnitTest() (dbm.DB, sdk.MultiStore, *sdk.KVStoreKey, *sdk.KVStoreKey, *sdk.KVStoreKey) { db := dbm.NewMemDB() capKey := sdk.NewKVStoreKey("capkey") capKey2 := sdk.NewKVStoreKey("capkey2") + capKey3 := sdk.NewKVStoreKey("capkey3") ms := store.NewCommitMultiStore(db) ms.MountStoreWithDB(capKey, sdk.StoreTypeIAVL, db) ms.MountStoreWithDB(capKey2, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(capKey3, sdk.StoreTypeIAVL, db) ms.MountStoreWithDB(common.PairStoreKey, sdk.StoreTypeIAVL, db) ms.LoadLatestVersion() - return db, ms, capKey, capKey2 + return db, ms, capKey, capKey2, capKey3 } // coins to more than cover the fee @@ -72,19 +79,19 @@ func NewNamedAccount(ctx sdk.Context, am auth.AccountKeeper, free int64) (crypto return privKey, appAcc } -func NewAccountForPub(ctx sdk.Context, am auth.AccountKeeper, free, locked, freeze int64) (crypto.PrivKey, sdk.Account) { +func NewAccountForPub(ctx sdk.Context, am auth.AccountKeeper, free, locked, freeze int64, symbol string) (crypto.PrivKey, sdk.Account) { privKey, addr := PrivAndAddr() acc := am.NewAccountWithAddress(ctx, addr) coins := NewNativeTokens(free) - coins = append(coins, sdk.NewCoin("XYZ-000", free)) + coins = append(coins, sdk.NewCoin(symbol, free)) acc.SetCoins(coins) appAcc := acc.(*types.AppAccount) lockedCoins := NewNativeTokens(locked) - lockedCoins = append(lockedCoins, sdk.NewCoin("XYZ-000", locked)) + lockedCoins = append(lockedCoins, sdk.NewCoin(symbol, locked)) appAcc.SetLockedCoins(lockedCoins) freezeCoins := NewNativeTokens(freeze) - freezeCoins = append(freezeCoins, sdk.NewCoin("XYZ-000", freeze)) + freezeCoins = append(freezeCoins, sdk.NewCoin(symbol, freeze)) appAcc.SetFrozenCoins(freezeCoins) am.SetAccount(ctx, acc) return privKey, acc diff --git a/common/types/mini_token.go b/common/types/mini_token.go new file mode 100644 index 000000000..0ffba0569 --- /dev/null +++ b/common/types/mini_token.go @@ -0,0 +1,212 @@ +package types + +import ( + "bytes" + "errors" + "fmt" + "regexp" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/binance-chain/node/common/utils" +) + +const ( + MiniTokenSymbolMaxLen = 8 + MiniTokenSymbolMinLen = 3 + MiniTokenSymbolSuffixLen = 4 // probably enough. if it collides (unlikely) the issuer can just use another tx. + MiniTokenSymbolTxHashSuffixLen = 3 // probably enough. if it collides (unlikely) the issuer can just use another tx. + MiniTokenSymbolMSuffix = "M" + + MiniTokenMinExecutionAmount int64 = 1e8 // 1 with 8 decimal digits + MiniTokenSupplyUpperBound int64 = 1000000e8 // 1m with 8 decimal digits + TinyTokenSupplyUpperBound int64 = 10000e8 // 10k with 8 decimal digits + MaxTokenURILength = 2048 + + TinyRangeType SupplyRangeType = 1 + MiniRangeType SupplyRangeType = 2 +) + +type SupplyRangeType int8 + +func (t SupplyRangeType) UpperBound() int64 { + switch t { + case TinyRangeType: + return TinyTokenSupplyUpperBound + case MiniRangeType: + return MiniTokenSupplyUpperBound + default: + return -1 + } +} + +func (t SupplyRangeType) String() string { + switch t { + case TinyRangeType: + return "Tiny" + case MiniRangeType: + return "Mini" + default: + return "Unknown" + } +} + +var SupplyRange = struct { + TINY SupplyRangeType + MINI SupplyRangeType +}{TinyRangeType, MiniRangeType} + +type MiniToken struct { + Name string `json:"name"` + Symbol string `json:"symbol"` + OrigSymbol string `json:"original_symbol"` + TotalSupply utils.Fixed8 `json:"total_supply"` + Owner sdk.AccAddress `json:"owner"` + Mintable bool `json:"mintable"` + TokenType SupplyRangeType `json:"token_type"` + TokenURI string `json:"token_uri"` //TODO set max length +} + +var _ IToken = &MiniToken{} + +func NewMiniToken(name, origSymbol, symbol string, supplyRangeType SupplyRangeType, totalSupply int64, owner sdk.AccAddress, mintable bool, tokenURI string) *MiniToken { + return &MiniToken{ + Name: name, + Symbol: symbol, + OrigSymbol: origSymbol, + TotalSupply: utils.Fixed8(totalSupply), + Owner: owner, + Mintable: mintable, + TokenType: supplyRangeType, + TokenURI: tokenURI, + } +} + +func (token MiniToken) GetName() string { + return token.Name +} + +func (token MiniToken) GetSymbol() string { + return token.Symbol +} + +func (token MiniToken) GetOrigSymbol() string { + return token.OrigSymbol +} + +func (token MiniToken) GetTotalSupply() utils.Fixed8 { + return token.TotalSupply +} + +func (token *MiniToken) SetTotalSupply(totalSupply utils.Fixed8) { + token.TotalSupply = totalSupply +} + +func (token MiniToken) GetOwner() sdk.AccAddress { + return token.Owner +} + +func (token MiniToken) IsMintable() bool { + return token.Mintable +} + +func (token *MiniToken) IsOwner(addr sdk.AccAddress) bool { + return bytes.Equal(token.Owner, addr) +} + +func (token MiniToken) String() string { + return fmt.Sprintf("{Name: %v, Symbol: %v, TokenType: %v, TotalSupply: %v, Owner: %X, Mintable: %v, TokenURI: %v}", + token.Name, token.Symbol, token.TokenType, token.TotalSupply, token.Owner, token.Mintable, token.TokenURI) +} + +//check if it's mini token by last letter without validation +func IsMiniTokenSymbol(symbol string) bool { + if symbol == NativeTokenSymbol || + symbol == NativeTokenSymbolDotBSuffixed { + return false + } + parts, err := splitSuffixedTokenSymbol(symbol) + if err != nil { + return false + } + suffixPart := parts[1] + + return len(suffixPart) == MiniTokenSymbolSuffixLen && strings.HasSuffix(suffixPart, MiniTokenSymbolMSuffix) +} + +//Validate and check if it's mini token +func IsValidMiniTokenSymbol(symbol string) bool { + return ValidateMiniTokenSymbol(symbol) == nil +} + +func ValidateIssueMiniSymbol(symbol string) error { + if len(symbol) == 0 { + return errors.New("token symbol cannot be empty") + } + + if symbol == NativeTokenSymbol || + symbol == NativeTokenSymbolDotBSuffixed { + return errors.New("symbol cannot be the same as native token") + } + + // check len without suffix + if symbolLen := len(symbol); symbolLen > MiniTokenSymbolMaxLen || symbolLen < MiniTokenSymbolMinLen { + return errors.New("length of token symbol is limited to 3~8") + } + + if !utils.IsAlphaNum(symbol) { + return errors.New("token symbol should be alphanumeric") + } + + return nil +} + +func ValidateMiniTokenSymbol(symbol string) error { + if len(symbol) == 0 { + return errors.New("suffixed token symbol cannot be empty") + } + + if symbol == NativeTokenSymbol || + symbol == NativeTokenSymbolDotBSuffixed { + return errors.New("symbol cannot be the same as native token") + } + + parts, err := splitSuffixedTokenSymbol(symbol) + if err != nil { + return err + } + + symbolPart := parts[0] + // check len without suffix + if len(symbolPart) < MiniTokenSymbolMinLen { + return fmt.Errorf("mini-token symbol part is too short, got %d chars", len(symbolPart)) + } + if len(symbolPart) > MiniTokenSymbolMaxLen { + return fmt.Errorf("mini-token symbol part is too long, got %d chars", len(symbolPart)) + } + + if !utils.IsAlphaNum(symbolPart) { + return errors.New("mini-token symbol part should be alphanumeric") + } + + suffixPart := parts[1] + if len(suffixPart) != MiniTokenSymbolSuffixLen { + return fmt.Errorf("mini-token symbol suffix must be %d chars in length, got %d", MiniTokenSymbolSuffixLen, len(suffixPart)) + } + + if suffixPart[len(suffixPart)-1:] != MiniTokenSymbolMSuffix { + return fmt.Errorf("mini-token symbol suffix must end with M") + } + + // prohibit non-hexadecimal chars in the suffix part + isHex, err := regexp.MatchString(fmt.Sprintf("[0-9A-F]{%d}M", MiniTokenSymbolTxHashSuffixLen), suffixPart) + if err != nil { + return err + } + if !isHex { + return fmt.Errorf("mini-token symbol tx hash suffix must be hex with a length of %d", MiniTokenSymbolTxHashSuffixLen) + } + + return nil +} diff --git a/common/types/token.go b/common/types/token.go index fb7ab19e9..a3c1ae974 100644 --- a/common/types/token.go +++ b/common/types/token.go @@ -26,6 +26,20 @@ const ( NativeTokenTotalSupply = 2e16 ) +type IToken interface { + GetName() string + GetSymbol() string + GetOrigSymbol() string + GetTotalSupply() utils.Fixed8 + SetTotalSupply(totalSupply utils.Fixed8) + GetOwner() sdk.AccAddress + IsMintable() bool + IsOwner(addr sdk.AccAddress) bool + String() string +} + +var _ IToken = &Token{} + type Token struct { Name string `json:"name"` Symbol string `json:"symbol"` @@ -35,9 +49,37 @@ type Token struct { Mintable bool `json:"mintable"` } +func (token Token) GetName() string { + return token.Name +} + +func (token Token) GetSymbol() string { + return token.Symbol +} + +func (token Token) GetOrigSymbol() string { + return token.OrigSymbol +} + +func (token Token) GetTotalSupply() utils.Fixed8 { + return token.TotalSupply +} + +func (token *Token) SetTotalSupply(totalSupply utils.Fixed8) { + token.TotalSupply = totalSupply +} + +func (token Token) GetOwner() sdk.AccAddress { + return token.Owner +} + +func (token Token) IsMintable() bool { + return token.Mintable +} + func NewToken(name, symbol string, totalSupply int64, owner sdk.AccAddress, mintable bool) (*Token, error) { // double check that the symbol is suffixed - if err := ValidateMapperTokenSymbol(symbol); err != nil { + if err := ValidateTokenSymbol(symbol); err != nil { return nil, err } parts, err := splitSuffixedTokenSymbol(symbol) @@ -60,19 +102,7 @@ func (token Token) String() string { token.Name, token.Symbol, token.TotalSupply, token.Owner, token.Mintable) } -// Token Validation - -func ValidateToken(token Token) error { - if err := ValidateMapperTokenSymbol(token.Symbol); err != nil { - return err - } - if err := ValidateIssueMsgTokenSymbol(token.OrigSymbol); err != nil { - return err - } - return nil -} - -func ValidateIssueMsgTokenSymbol(symbol string) error { +func ValidateIssueSymbol(symbol string) error { if len(symbol) == 0 { return errors.New("token symbol cannot be empty") } @@ -93,7 +123,17 @@ func ValidateIssueMsgTokenSymbol(symbol string) error { return nil } -func ValidateMapperTokenSymbol(symbol string) error { +func ValidateTokenSymbols(coins sdk.Coins) error { + for _, coin := range coins { + err := ValidateTokenSymbol(coin.Denom) + if err != nil { + return err + } + } + return nil +} + +func ValidateTokenSymbol(symbol string) error { if len(symbol) == 0 { return errors.New("suffixed token symbol cannot be empty") } diff --git a/common/types/token_test.go b/common/types/token_test.go index b5fb83353..f92ceaca6 100644 --- a/common/types/token_test.go +++ b/common/types/token_test.go @@ -79,30 +79,30 @@ func TestNewToken(t *testing.T) { } } -func TestValidateIssueMsgTokenSymbol(t *testing.T) { +func TestValidateIssueSymbol(t *testing.T) { for _, tt := range issueMsgSymbolTestCases { t.Run(tt.symbol, func(t *testing.T) { - if err := types.ValidateIssueMsgTokenSymbol(tt.symbol); (err == nil) != tt.correct { - t.Errorf("ValidateIssueMsgTokenSymbol() error = %v, correct %v", err, tt.correct) + if err := types.ValidateIssueSymbol(tt.symbol); (err == nil) != tt.correct { + t.Errorf("ValidateIssueSymbol() error = %v, correct %v", err, tt.correct) } }) } - // extra test. an issued symbol that is valid in NewToken and ValidateMapperTokenSymbol but not here - if err := types.ValidateIssueMsgTokenSymbol("XYZ-000"); err == nil { - t.Errorf("ValidateIssueMsgTokenSymbol() error = %v, expected XYZ-000 to be invalid", err) + // extra test. an issued symbol that is valid in NewToken and ValidateTokenSymbol but not here + if err := types.ValidateIssueSymbol("XYZ-000"); err == nil { + t.Errorf("ValidateIssueSymbol() error = %v, expected XYZ-000 to be invalid", err) } } -func TestValidateMapperTokenSymbol(t *testing.T) { +func TestValidateTokenSymbol(t *testing.T) { for _, tt := range tokenMapperSymbolTestCases { t.Run(tt.symbol, func(t *testing.T) { - if err := types.ValidateMapperTokenSymbol(tt.symbol); (err == nil) != tt.correct { - t.Errorf("ValidateMapperTokenSymbol() error = %v, correct %v", err, tt.correct) + if err := types.ValidateTokenSymbol(tt.symbol); (err == nil) != tt.correct { + t.Errorf("ValidateTokenSymbol() error = %v, correct %v", err, tt.correct) } }) } - // extra test. an issued symbol that is valid in ValidateIssueMsgTokenSymbol but not here - if err := types.ValidateMapperTokenSymbol("XYZ"); err == nil { - t.Errorf("ValidateIssueMsgTokenSymbol() error = %v, expected XYZ to be invalid", err) + // extra test. an issued symbol that is valid in ValidateIssueSymbol but not here + if err := types.ValidateTokenSymbol("XYZ"); err == nil { + t.Errorf("ValidateIssueSymbol() error = %v, expected XYZ to be invalid", err) } } diff --git a/common/types/wire.go b/common/types/wire.go index 90e3161ae..7f5b56a25 100644 --- a/common/types/wire.go +++ b/common/types/wire.go @@ -10,7 +10,10 @@ func RegisterWire(cdc *wire.Codec) { // Register AppAccount cdc.RegisterInterface((*sdk.Account)(nil), nil) cdc.RegisterInterface((*NamedAccount)(nil), nil) + cdc.RegisterInterface((*IToken)(nil), nil) + cdc.RegisterConcrete(&AppAccount{}, "bnbchain/Account", nil) - cdc.RegisterConcrete(Token{}, "bnbchain/Token", nil) + cdc.RegisterConcrete(&Token{}, "bnbchain/Token", nil) + cdc.RegisterConcrete(&MiniToken{}, "bnbchain/MiniToken", nil) } diff --git a/common/upgrade/upgrade.go b/common/upgrade/upgrade.go index d18a3e8b2..41ac806bd 100644 --- a/common/upgrade/upgrade.go +++ b/common/upgrade/upgrade.go @@ -18,16 +18,16 @@ const ( // Archimedes Upgrade BEP3 = "BEP3" // https://github.com/binance-chain/BEPs/pull/30 - // TODO: add upgrade name FixSignBytesOverflow = sdk.FixSignBytesOverflow LotSizeOptimization = "LotSizeOptimization" ListingRuleUpgrade = "ListingRuleUpgrade" // Remove restriction that only the owner of base asset can list trading pair FixZeroBalance = "FixZeroBalance" - BEP67 = "BEP67" // https://github.com/binance-chain/BEPs/pull/67 - // BUSD Pair Upgrade - BEP70 = "BEP70" // https://github.com/binance-chain/BEPs/pull/70 + //Nightingale upgrade + BEP8 = sdk.BEP8 // https://github.com/binance-chain/BEPs/pull/69 Mini token upgrade + BEP67 = "BEP67" // https://github.com/binance-chain/BEPs/pull/67 Expiry time upgrade + BEP70 = "BEP70" // https://github.com/binance-chain/BEPs/pull/70 BUSD Pair Upgrade ) func UpgradeBEP10(before func(), after func()) { diff --git a/go.mod b/go.mod index bf19909ad..4d256f838 100644 --- a/go.mod +++ b/go.mod @@ -29,9 +29,9 @@ require ( ) replace ( - github.com/cosmos/cosmos-sdk => github.com/binance-chain/bnc-cosmos-sdk v0.25.0-binance.20 + github.com/cosmos/cosmos-sdk => github.com/binance-chain/bnc-cosmos-sdk v0.25.0-binance.22 github.com/tendermint/go-amino => github.com/binance-chain/bnc-go-amino v0.14.1-binance.2 - github.com/tendermint/iavl => github.com/binance-chain/bnc-tendermint-iavl v0.12.0-binance.3 + github.com/tendermint/iavl => github.com/binance-chain/bnc-tendermint-iavl v0.12.0-binance.4 github.com/tendermint/tendermint => github.com/binance-chain/bnc-tendermint v0.32.3-binance.1 github.com/zondax/ledger-cosmos-go => github.com/binance-chain/ledger-cosmos-go v0.9.9-binance.3 golang.org/x/crypto => github.com/tendermint/crypto v0.0.0-20190823143015-45b1026d81ae diff --git a/go.sum b/go.sum index 92d3c70aa..bfdcf35ae 100644 --- a/go.sum +++ b/go.sum @@ -26,14 +26,14 @@ github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/binance-chain/bnc-cosmos-sdk v0.25.0-binance.20 h1:KwluEf2oheva5zdqeO/c/trvVFtrlYMyPXrCoTAAsTc= -github.com/binance-chain/bnc-cosmos-sdk v0.25.0-binance.20/go.mod h1:Qd+w7Vm2WGxOqPquawShZ8KsHtA8PVDnx6XvZobloYg= +github.com/binance-chain/bnc-cosmos-sdk v0.25.0-binance.22 h1:AYJZDs9H3DN/tETOMtRQOpMv2JVxCpxhciSttofQJY0= +github.com/binance-chain/bnc-cosmos-sdk v0.25.0-binance.22/go.mod h1:lE+RVRYNWqO8xUa/SjiqyDGiII5BtjMSwovI/lcbSuM= github.com/binance-chain/bnc-go-amino v0.14.1-binance.2 h1:XcbcfisVItk92UKoGbtNT8nbcfadj3H3ayuM2srAfVs= github.com/binance-chain/bnc-go-amino v0.14.1-binance.2/go.mod h1:yaElUUxWtv/TC/ldGtlKAvS1vKwokxgJ1d97I+6is80= github.com/binance-chain/bnc-tendermint v0.32.3-binance.1 h1:LDGvORYLSwsTEQM0W7yrbdgjrAZxQDe18WUTLNuFOEc= github.com/binance-chain/bnc-tendermint v0.32.3-binance.1/go.mod h1:kN5dNxE8voFtDqx2HjbM8sBIH5cUuMtLg0XEHjqzUiY= -github.com/binance-chain/bnc-tendermint-iavl v0.12.0-binance.3 h1:OyTJet9aGz+c2WKpTf5cAvJNiQeqVFYP4AV9cPpro2M= -github.com/binance-chain/bnc-tendermint-iavl v0.12.0-binance.3/go.mod h1:Zmh8GRdNJB8DULIOBar3JCZp6tSpcvM1NGKfE9U2EzA= +github.com/binance-chain/bnc-tendermint-iavl v0.12.0-binance.4 h1:BhaV2iiGWfRC6iB8HHOYJeUDwtQMB2pUA4ah+KCbBhI= +github.com/binance-chain/bnc-tendermint-iavl v0.12.0-binance.4/go.mod h1:Zmh8GRdNJB8DULIOBar3JCZp6tSpcvM1NGKfE9U2EzA= github.com/binance-chain/ledger-cosmos-go v0.9.9-binance.3 h1:FFpFbkzlP2HUyxQCm0eoU6mkfgMNynfqZRbeWqlaLdQ= github.com/binance-chain/ledger-cosmos-go v0.9.9-binance.3/go.mod h1:TULULYTvPuWBxFIZFy6KjJaxJzbHeUderYNB1YhD6N0= github.com/binance-chain/tss v0.1.2 h1:AyTedSG5HG/WAvM9PDPWjTXQ+dvNdHg3x1c+1a584PQ= diff --git a/integration_test.sh b/integration_test.sh index d147954a2..84d49dc0b 100755 --- a/integration_test.sh +++ b/integration_test.sh @@ -75,7 +75,6 @@ bob_addr=$(./bnbcli keys list --home ${cli_home} | grep bob | grep -o "bnb1[0-9a # wait for the chain sleep 10s - ## ROUND 1 ## # send @@ -320,4 +319,63 @@ sleep 1s result=$(expect ./deposit.exp ${swapID} "10000:${eth_symbol}" bob ${chain_id} ${cli_home}) check_operation "Deposit to a deposited single chain atomic swap" "${result}" "ERROR" + +## ROUND 5 ## + +sleep 1s +# issue token +result=$(expect ./issue_mini.exp MBC MiniBitcoin 900000000000 true alice ${chain_id} ${cli_home} 1 sample) +mbc_symbol=$(echo "${result}" | tail -n 1 | grep -o "MBC-[0-9A-Z]*M") +check_operation "Issue Mini Token" "${result}" "${chain_operation_words}" + + +sleep 1s +# send +result=$(expect ./send.exp ${cli_home} alice ${chain_id} "100000000000:${mbc_symbol}" ${bob_addr} 1) +check_operation "Send Token" "${result}" "${chain_operation_words}" + +sleep 1s +# multi send +echo ${bob_addr} +result=$(expect ./multi_send.exp ${cli_home} alice ${chain_id} "[{\"to\":\"${bob_addr}\",\"amount\":\"10000000000:${mbc_symbol}\"},{\"to\":\"${alice_addr}\",\"amount\":\"1000000000:${mbc_symbol}\"}]" 1) +check_operation "Multi Send Token" "${result}" "${chain_operation_words}" + +sleep 1s +# mint token +result=$(expect ./mint.exp ${mbc_symbol} 10000000000 alice ${chain_id} ${cli_home}) +check_operation "Mint Token" "${result}" "${chain_operation_words}" + +sleep 3s +# list trading pair +result=$(expect ./list_mini.exp ${mbc_symbol} BNB 100000000 alice ${chain_id} ${cli_home}) +check_operation "List Trading Pair" "${result}" "${chain_operation_words}" + +sleep 1s +# place buy order +result=$(expect ./order.exp ${mbc_symbol}_BNB 1 100000000 1000000000 bob ${chain_id} gte ${cli_home}) +check_operation "Place Order" "${result}" "${chain_operation_words}" +order_id=$(echo "${result}" | tail -n 1 | grep -o "[0-9A-Z]\{4,\}-[0-9]*") # capture order id, not symbol +printf "Order ID: $order_id\n" + +sleep 2s +# cancel order +result=$(expect ./cancel.exp "${mbc_symbol}_BNB" "${order_id}" bob ${chain_id} ${cli_home}) +check_operation "Cancel Order" "${result}" "${chain_operation_words}" + +sleep 1s +# place buy order +result=$(expect ./order.exp ${mbc_symbol}_BNB 1 100000000 1000000000 bob ${chain_id} gte ${cli_home}) +check_operation "Place Order" "${result}" "${chain_operation_words}" + +echo "" +./bnbcli dex show -l ${mbc_symbol}_BNB --trust-node true + +sleep 1s +# place Sell order +result=$(expect ./order.exp ${mbc_symbol}_BNB 2 100000000 2000000000 alice ${chain_id} gte ${cli_home}) +check_operation "Place Order" "${result}" "${chain_operation_words}" + +result=$(./bnbcli dex show -l ${mbc_symbol}_BNB --trust-node true) +check_operation "Order Book" "${result}" "${order_book_words}" + exit_test 0 diff --git a/networks/demo/issue_mini.exp b/networks/demo/issue_mini.exp new file mode 100755 index 000000000..39a86f5b3 --- /dev/null +++ b/networks/demo/issue_mini.exp @@ -0,0 +1,21 @@ +#!/usr/bin/expect + +set symbol [lindex $argv 0] +set token_name [lindex $argv 1] +set supply [lindex $argv 2] +set mintable [lindex $argv 3] +set from [lindex $argv 4] +set chain_id [lindex $argv 5] +set home [lindex $argv 6] +set type [lindex $argv 7] +set uri [lindex $argv 8] + +set timeout 30 + if {"${home}" == ""} { + spawn ./bnbcli token issue-mini -s $symbol --token-name $token_name -n $supply --mintable $mintable --from $from --token-uri $uri --chain-id $chain_id + } else { + spawn ./bnbcli token issue-mini --home $home -s $symbol --token-name $token_name -n $supply --mintable $mintable --from $from --token-uri $uri --chain-id $chain_id + } + expect "Password*" + send "12345678\r" +interact diff --git a/networks/demo/list_mini.exp b/networks/demo/list_mini.exp new file mode 100755 index 000000000..58cf6f515 --- /dev/null +++ b/networks/demo/list_mini.exp @@ -0,0 +1,19 @@ +#!/usr/bin/expect + +set symbol [lindex $argv 0] +set quote_symbol [lindex $argv 1] +set init_price [lindex $argv 2] +set from [lindex $argv 3] +set chain_id [lindex $argv 4] +set home [lindex $argv 5] +set proposal_id [lindex $argv 6] + +set timeout 30 + if {"${home}" == ""} { + spawn ./bnbcli dex list-mini -s $symbol --quote-asset-symbol $quote_symbol --from $from --init-price $init_price --chain-id $chain_id + } else { + spawn ./bnbcli dex list-mini --home $home -s $symbol --quote-asset-symbol $quote_symbol --from $from --init-price $init_price --chain-id $chain_id + } + expect "Password*" + send "12345678\r" +interact diff --git a/networks/demo/multi_send.exp b/networks/demo/multi_send.exp index 3f634f171..4e62bf174 100755 --- a/networks/demo/multi_send.exp +++ b/networks/demo/multi_send.exp @@ -4,9 +4,10 @@ set home [lindex $argv 0] set from [lindex $argv 1] set chain_id [lindex $argv 2] set tx [lindex $argv 3] +set memo [lindex $argv 4] set timeout 30 - spawn ./bnbcli token multi-send --home $home --from $from --chain-id=$chain_id --transfers $tx + spawn ./bnbcli token multi-send --home $home --from $from --chain-id=$chain_id --transfers $tx --memo $memo expect "Password*" send "12345678\r" interact diff --git a/networks/publisher/list_mini.sh b/networks/publisher/list_mini.sh new file mode 100755 index 000000000..c918511a0 --- /dev/null +++ b/networks/publisher/list_mini.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# start a validator and witness on local machine +# later on db indexer and publisher can be setup and started by newIndexer.sh and newPublisher.sh +# later on order generation can be kicked off by ordergen.sh +# TODO: support two validator without docker in same machine + +########################### SETUP ######################### +home=$HOME +src="${home}/go/src/github.com/binance-chain/node" +deamonhome="${home}/.bnbchaind" +witnesshome="${home}/.bnbchaind_witness" +clihome="${home}/.bnbcli" +chain_id='test-chain-n4b735' +echo $src +echo $deamonhome +echo $witnesshome +echo $clihome + +key_seed_path="${home}" +executable="${src}/build/bnbchaind" +clipath="${src}/build/bnbcli" +cli="${clipath} --home ${clihome}" +scripthome="${src}/networks/publisher" +############################ END ########################## + +#X1Mini_symbol="X1Mini-ED3" +result=$(${cli} token issue-tiny --from=zc --token-name="X1M Coin" --symbol=X1M --total-supply=800000000000 --chain-id ${chain_id}) +X1Mini_symbol=$(echo "${result}" | tail -n 1 | grep -o "X1M-[0-9A-Z]*") +echo ${X1Mini_symbol} +sleep 2 +echo 1234qwerasdf|${cli} dex list-mini -s=${X1Mini_symbol} --quote-asset-symbol=BNB --init-price=100000000 --from=zc --chain-id ${chain_id} +sleep 1 +zz_addr=$(${cli} keys list | grep "zz.*local" | grep -o "bnb[0-9a-zA-Z]*" | grep -v "bnbp") +echo 1234qwerasdf|${cli} send --from=zc --to=${zz_addr} --amount=200000000000:${X1Mini_symbol} --chain-id ${chain_id} +sleep 5 diff --git a/networks/publisher/newIndexer.sh b/networks/publisher/newIndexer.sh index 043af5ece..307279218 100755 --- a/networks/publisher/newIndexer.sh +++ b/networks/publisher/newIndexer.sh @@ -1,12 +1,16 @@ #!/usr/bin/env bash ########################### SETUP ######################### -src='/Users/zhaocong/go/src/github.com/binance-chain/node' -home='/Users/zhaocong' -deamonhome='/Users/zhaocong/.bnbchaind' -witnesshome='/Users/zhaocong/.bnbchaind_indexer' -clihome='/Users/zhaocong/.bnbcli' +home=$HOME +src="${home}/go/src/github.com/binance-chain/node" +deamonhome="${home}/.bnbchaind" +witnesshome="${home}/.bnbchaind_witness" +clihome="${home}/.bnbcli" chain_id='test-chain-n4b735' +echo $src +echo $deamonhome +echo $witnesshome +echo $clihome key_seed_path="${home}" executable="${src}/build/bnbchaind" diff --git a/networks/publisher/newPublisher.sh b/networks/publisher/newPublisher.sh index 951bfa248..f70da490b 100755 --- a/networks/publisher/newPublisher.sh +++ b/networks/publisher/newPublisher.sh @@ -1,12 +1,16 @@ #!/usr/bin/env bash ########################### SETUP ######################### -src='/Users/zhaocong/go/src/github.com/binance-chain/node' -home='/Users/zhaocong' -deamonhome='/Users/zhaocong/.bnbchaind' -witnesshome='/Users/zhaocong/.bnbchaind_publisher' -clihome='/Users/zhaocong/.bnbcli' +home=$HOME +src="${home}/go/src/github.com/binance-chain/node" +deamonhome="${home}/.bnbchaind" +witnesshome="${home}/.bnbchaind_publisher" +clihome="${home}/.bnbcli" chain_id='test-chain-n4b735' +echo $src +echo $deamonhome +echo $witnesshome +echo $clihome key_seed_path="${home}" executable="${src}/build/bnbchaind" diff --git a/networks/publisher/newValidator.sh b/networks/publisher/newValidator.sh index 8bdcca4b4..a4915fdd5 100755 --- a/networks/publisher/newValidator.sh +++ b/networks/publisher/newValidator.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash -src='/Users/zhaocong/go/src/github.com/binance-chain/node' -executable='/Users/zhaocong/go/src/github.com/binance-chain/node/build/bnbchaind' -cli='/Users/zhaocong/go/src/github.com/binance-chain/node/build/bnbcli' -home='/Users/zhaocong' +home=$HOME +src="${home}/go/src/github.com/binance-chain/node" +executable="${home}/go/src/github.com/binance-chain/node/build/bnbchaind" +cli="${home}/go/src/github.com/binance-chain/node/build/bnbcli" ## clean history data #rm -r ${home}/.bnbchaind_val2 diff --git a/networks/publisher/ordergen.sh b/networks/publisher/ordergen.sh index ea9ffb8bc..d755ff4f0 100755 --- a/networks/publisher/ordergen.sh +++ b/networks/publisher/ordergen.sh @@ -1,11 +1,12 @@ #!/bin/bash ########################### SETUP ######################### -clipath='/Users/zhaocong/go/src/github.com/binance-chain/node/build/bnbcli' -home='/Users/zhaocong' -clihome='/Users/zhaocong/.bnbcli' +home=$HOME +src="${home}/go/src/github.com/binance-chain/node" +clipath="${home}/go/src/github.com/binance-chain/node/build/bnbcli" +clihome="${home}/.bnbcli" chainId='test-chain-n4b735' # should be same with publisher/setup.sh or testnet/deploy.sh -src='/Users/zhaocong/go/src/github.com/binance-chain/node' + cli="${clipath} --home ${clihome}" scripthome="${src}/networks/publisher" @@ -38,17 +39,21 @@ function random() while : do side=$(random 1 2) - price=$(random 1 20) - qty=$(random 10 20) + price=$(random 1 4) + qty=$(random 1 2) pause=$(random 5 7) symbolNum=$(random 1 10) - symbol="NNB-FCA_BNB" + symbol="ZCK-980_BNB" if [ $symbolNum -lt 4 ] then - symbol="NNB-FCA_BNB" + symbol="Y2B-822M_BNB" + elif [ $symbolNum -lt 6 ] + then + symbol="X1M-42FM_BNB" + else [ $symbolNum -lt 8 ] + symbol="ZCK-CD6_BNB" fi - from="zc" if [ $side == 1 ] then @@ -57,11 +62,11 @@ do printf "\n${cli} dex order --symbol=${symbol} --side=${side} --price=${price}00000000 --qty=${qty}00000000 --tif="GTE" --from=${from} --chain-id=${chainId}\n" - ${cli} dex order --symbol=${symbol} --side=${side} --price=${price}00000000 --qty=${qty}00000000 --tif="GTE" --from=${from} --chain-id=${chainId} + echo 1234qwerasdf|${cli} dex order --symbol=${symbol} --side=${side} --price=${price}00000000 --qty=${qty}00000000 --tif="GTE" --from=${from} --chain-id=${chainId} # -d is used for get response of expect script. TODO: better log redirection -# result=$(expect -d ${scripthome}/ordergen.exp "${clipath}" "${clihome}" "${symbol}" "${side}" "${price}00000000" "${qty}00000000" "${from}" "${chainId}") + #result=$(expect -d ${scripthome}/ordergen.exp "${clipath}" "${clihome}" "${symbol}" "${side}" "${price}00000000" "${qty}00000000" "${from}" "${chainId}") -# printf "\nsleep ${pause} seconds...\n" -# sleep ${pause} + #printf "\nsleep ${pause} seconds...\n" + sleep ${pause} done \ No newline at end of file diff --git a/networks/publisher/setup.sh b/networks/publisher/setup.sh index 12fe03445..a859b98c7 100755 --- a/networks/publisher/setup.sh +++ b/networks/publisher/setup.sh @@ -6,12 +6,16 @@ # TODO: support two validator without docker in same machine ########################### SETUP ######################### -src='/Users/zhaocong/go/src/github.com/binance-chain/node' -home='/Users/zhaocong' -deamonhome='/Users/zhaocong/.bnbchaind' -witnesshome='/Users/zhaocong/.bnbchaind_witness' -clihome='/Users/zhaocong/.bnbcli' +home=$HOME +src="${home}/go/src/github.com/binance-chain/node" +deamonhome="${home}/.bnbchaind" +witnesshome="${home}/.bnbchaind_witness" +clihome="${home}/.bnbcli" chain_id='test-chain-n4b735' +echo $src +echo $deamonhome +echo $witnesshome +echo $clihome key_seed_path="${home}" executable="${src}/build/bnbchaind" @@ -40,6 +44,8 @@ mkdir -p ${home}/.bnbchaind_witness/config sed -i -e "s/log_level = \"main:info,state:info,\*:error\"/log_level = \"debug\"/g" ${deamonhome}/config/config.toml sed -i -e "s/allow_duplicate_ip = false/allow_duplicate_ip = true/g" ${deamonhome}/config/config.toml sed -i -e "s/addr_book_strict = true/addr_book_strict = false/g" ${deamonhome}/config/config.toml +# for http-ap testing +sed -i -e "s/index_tags = \"\"/index_tags = \"tx.height\"/g" ${deamonhome}/config/config.toml sed -i -e 's/logToConsole = true/logToConsole = false/g' ${deamonhome}/config/app.toml sed -i -e 's/breatheBlockInterval = 0/breatheBlockInterval = 100/g' ${deamonhome}/config/app.toml diff --git a/networks/publisher/setup_mini.sh b/networks/publisher/setup_mini.sh new file mode 100755 index 000000000..ba56770b4 --- /dev/null +++ b/networks/publisher/setup_mini.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash + +# start a validator and witness on local machine +# later on db indexer and publisher can be setup and started by newIndexer.sh and newPublisher.sh +# later on order generation can be kicked off by ordergen.sh +# TODO: support two validator without docker in same machine + +########################### SETUP ######################### +home=$HOME +src="${home}/go/src/github.com/binance-chain/node" +deamonhome="${home}/.bnbchaind" +witnesshome="${home}/.bnbchaind_witness" +clihome="${home}/.bnbcli" +chain_id='test-chain-n4b735' +echo $src +echo $deamonhome +echo $witnesshome +echo $clihome + +key_seed_path="${home}" +executable="${src}/build/bnbchaind" +clipath="${src}/build/bnbcli" +cli="${clipath} --home ${clihome}" +scripthome="${src}/networks/publisher" +############################ END ########################## + +# clean history data +rm -r ${deamonhome} +rm -r ${clihome} +rm -r ${home}/.bnbchaind_witness + +# build +cd ${src} +make build + +# init a validator and witness node +${executable} init --moniker xxx --chain-id ${chain_id} > ${key_seed_path}/key_seed.json # cannot save into ${deamonhome}/init.json +secret=$(cat ${key_seed_path}/key_seed.json | grep secret | grep -o ":.*" | grep -o "\".*" | sed "s/\"//g") +#echo ${secret} + +mkdir -p ${home}/.bnbchaind_witness/config + +#sed -i -e "s/skip_timeout_commit = false/skip_timeout_commit = true/g" ${deamonhome}/config/config.toml +sed -i -e "s/log_level = \"main:info,state:info,\*:error\"/log_level = \"debug\"/g" ${deamonhome}/config/config.toml +sed -i -e "s/allow_duplicate_ip = false/allow_duplicate_ip = true/g" ${deamonhome}/config/config.toml +sed -i -e "s/addr_book_strict = true/addr_book_strict = false/g" ${deamonhome}/config/config.toml +# for http-ap testing +sed -i -e "s/index_tags = \"\"/index_tags = \"tx.height\"/g" ${deamonhome}/config/config.toml + +sed -i -e 's/logToConsole = true/logToConsole = false/g' ${deamonhome}/config/app.toml +sed -i -e 's/breatheBlockInterval = 0/breatheBlockInterval = 100/g' ${deamonhome}/config/app.toml +sed -i -e "s/publishOrderUpdates = false/publishOrderUpdates = true/g" ${deamonhome}/config/app.toml +sed -i -e "s/publishAccountBalance = false/publishAccountBalance = true/g" ${deamonhome}/config/app.toml +sed -i -e "s/publishOrderBook = false/publishOrderBook = true/g" ${deamonhome}/config/app.toml +sed -i -e "s/publishBlockFee = false/publishBlockFee = true/g" ${deamonhome}/config/app.toml +sed -i -e "s/publishTransfer = false/publishTransfer = true/g" ${deamonhome}/config/app.toml +sed -i -e "s/publishLocal = false/publishLocal = true/g" ${deamonhome}/config/app.toml + +# config witness node +cp ${deamonhome}/config/genesis.json ${witnesshome}/config/ +cp ${deamonhome}/config/app.toml ${witnesshome}/config/ +cp ${deamonhome}/config/config.toml ${witnesshome}/config/ + +sed -i -e "s/26/27/g" ${witnesshome}/config/config.toml +sed -i -e "s/6060/7060/g" ${witnesshome}/config/config.toml +#sed -i -e "s/fastest_sync_height = -1/fastest_sync_height = 10/g" ${witnesshome}/config/config.toml + +# start validator +${executable} start --pruning breathe > ${deamonhome}/log.txt 2>&1 & +validator_pid=$! +echo ${validator_pid} +sleep 20 # sleep in case cli status call failed to get node id +validatorStatus=$(${cli} status) +validator_id=$(echo ${validatorStatus} | grep -o "\"id\":\"[a-zA-Z0-9]*\"" | sed "s/\"//g" | sed "s/id://g") +#echo ${validator_id} + +# set witness peer to validator and start witness +sed -i -e "s/persistent_peers = \"\"/persistent_peers = \"${validator_id}@127.0.0.1:26656\"/g" ${witnesshome}/config/config.toml +sed -i -e "s/state_sync_height = -1/state_sync_height = 0/g" ${witnesshome}/config/config.toml +${executable} start --pruning breathe --home ${witnesshome} > ${witnesshome}/log.txt 2>&1 & +witness_pid=$! +echo ${witness_pid} + +# init accounts +result=$(expect ${scripthome}/recover.exp "${secret}" "zc" "${clipath}" "${clihome}") +result=$(expect ${scripthome}/add_key.exp "zz" "${clipath}" "${clihome}") +zz_addr=$(${cli} keys list | grep "zz.*local" | grep -o "bnb[0-9a-zA-Z]*" | grep -v "bnbp") + +sleep 5 +## issue&list NNB and ZCB for ordergen +#result=$(${cli} token issue --from=zc --token-name="New BNB Coin" --symbol=NNB --total-supply=2000000000000000 --chain-id ${chain_id}) +#nnb_symbol=$(echo "${result}" | tail -n 1 | grep -o "NNB-[0-9A-Z]*") +#echo ${nnb_symbol} +#sleep 5 +#((expire_time=$(date '+%s')+1000)) +#${cli} gov submit-list-proposal --chain-id ${chain_id} --from zc --deposit 200000000000:BNB --base-asset-symbol ${nnb_symbol} --quote-asset-symbol BNB --init-price 1000000000 --title "list NNB/BNB" --description "list NNB/BNB" --expire-time ${expire_time} --voting-period 8 --json +#sleep 2 +#${cli} gov vote --from zc --chain-id ${chain_id} --proposal-id 1 --option Yes --json +#sleep 7 +#${cli} dex list -s=${nnb_symbol} --quote-asset-symbol=BNB --init-price=1000000000 --from=zc --chain-id ${chain_id} --proposal-id 1 +#sleep 1 +#result=$(${cli} token issue --from=zc --token-name="ZC Coin" --symbol=ZCB --total-supply=2000000000000000 --chain-id ${chain_id}) +#zcb_symbol=$(echo "${result}" | tail -n 1 | grep -o "ZCB-[0-9A-Z]*") +#echo ${zcb_symbol} +#sleep 5 +#((expire_time=$(date '+%s')+1000)) +#${cli} gov submit-list-proposal --chain-id ${chain_id} --from zc --deposit 200000000000:BNB --base-asset-symbol ${zcb_symbol} --quote-asset-symbol BNB --init-price 1000000000 --title "list NNB/BNB" --description "list NNB/BNB" --expire-time ${expire_time} --voting-period 5 --json +#sleep 2 +#${cli} gov vote --from zc --chain-id ${chain_id} --proposal-id 2 --option Yes --json +#sleep 6 +#${cli} dex list -s=${zcb_symbol} --quote-asset-symbol=BNB --init-price=1000000000 --from=zc --chain-id ${chain_id} --proposal-id 2 +#sleep 1 +${cli} send --from=zc --to=${zz_addr} --amount=1000000000000000:BNB --chain-id ${chain_id} +#sleep 5 diff --git a/networks/tools/snapshot_viewer/snapshot.go b/networks/tools/snapshot_viewer/snapshot.go index a79df3eb8..9b7a098c5 100644 --- a/networks/tools/snapshot_viewer/snapshot.go +++ b/networks/tools/snapshot_viewer/snapshot.go @@ -11,6 +11,7 @@ import ( "github.com/cosmos/cosmos-sdk/store" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/tendermint/go-amino" cryptoAmino "github.com/tendermint/tendermint/crypto/encoding/amino" "github.com/tendermint/tendermint/libs/db" diff --git a/plugins/api/handlers.go b/plugins/api/handlers.go index 7757c2403..45c8ad6b3 100644 --- a/plugins/api/handlers.go +++ b/plugins/api/handlers.go @@ -7,6 +7,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/context" hnd "github.com/binance-chain/node/plugins/api/handlers" + "github.com/binance-chain/node/plugins/dex" dexapi "github.com/binance-chain/node/plugins/dex/client/rest" paramapi "github.com/binance-chain/node/plugins/param/client/rest" tksapi "github.com/binance-chain/node/plugins/tokens/client/rest" @@ -94,8 +95,12 @@ func (s *server) handleSimulateReq(cdc *wire.Codec, ctx context.CLIContext) http return s.withTextPlainForm(s.limitReqSize(h)) } -func (s *server) handlePairsReq(cdc *wire.Codec, ctx context.CLIContext) http.HandlerFunc { - return dexapi.GetPairsReqHandler(cdc, ctx) +func (s *server) handleBEP2PairsReq(cdc *wire.Codec, ctx context.CLIContext) http.HandlerFunc { + return dexapi.GetPairsReqHandler(cdc, ctx, dex.DexAbciQueryPrefix) +} + +func (s *server) handleMiniPairsReq(cdc *wire.Codec, ctx context.CLIContext) http.HandlerFunc { + return dexapi.GetPairsReqHandler(cdc, ctx, dex.DexMiniAbciQueryPrefix) } func (s *server) handleDexDepthReq(cdc *wire.Codec, ctx context.CLIContext) http.HandlerFunc { @@ -112,11 +117,19 @@ func (s *server) handleDexOpenOrdersReq(cdc *wire.Codec, ctx context.CLIContext) } func (s *server) handleTokenReq(cdc *wire.Codec, ctx context.CLIContext) http.HandlerFunc { - return tksapi.GetTokenReqHandler(cdc, ctx) + return tksapi.GetTokenReqHandler(cdc, ctx, false) } func (s *server) handleTokensReq(cdc *wire.Codec, ctx context.CLIContext) http.HandlerFunc { - return tksapi.GetTokensReqHandler(cdc, ctx) + return tksapi.GetTokensReqHandler(cdc, ctx, false) +} + +func (s *server) handleMiniTokenReq(cdc *wire.Codec, ctx context.CLIContext) http.HandlerFunc { + return tksapi.GetTokenReqHandler(cdc, ctx, true) +} + +func (s *server) handleMiniTokensReq(cdc *wire.Codec, ctx context.CLIContext) http.HandlerFunc { + return tksapi.GetTokensReqHandler(cdc, ctx, true) } func (s *server) handleBalancesReq(cdc *wire.Codec, ctx context.CLIContext, tokens tkstore.Mapper) http.HandlerFunc { diff --git a/plugins/api/routes.go b/plugins/api/routes.go index 70b0c8874..8e259ab6e 100644 --- a/plugins/api/routes.go +++ b/plugins/api/routes.go @@ -29,7 +29,7 @@ func (s *server) bindRoutes() *server { Methods("POST") // dex routes - r.HandleFunc(prefix+"/markets", s.handlePairsReq(s.cdc, s.ctx)). + r.HandleFunc(prefix+"/markets", s.handleBEP2PairsReq(s.cdc, s.ctx)). Methods("GET") r.HandleFunc(prefix+"/depth", s.handleDexDepthReq(s.cdc, s.ctx)). Queries("symbol", "{symbol}", "limit", "{limit:[0-9]+}"). @@ -44,6 +44,9 @@ func (s *server) bindRoutes() *server { Queries("address", "{address}", "symbol", "{symbol}"). Methods("GET") + r.HandleFunc(prefix+"/mini/markets", s.handleMiniPairsReq(s.cdc, s.ctx)). + Methods("GET") + // tokens routes r.HandleFunc(prefix+"/tokens", s.handleTokensReq(s.cdc, s.ctx)). Methods("GET") @@ -54,6 +57,12 @@ func (s *server) bindRoutes() *server { r.HandleFunc(prefix+"/balances/{address}/{symbol}", s.handleBalanceReq(s.cdc, s.ctx, s.tokens)). Methods("GET") + // mini tokens routes + r.HandleFunc(prefix+"/mini/tokens", s.handleMiniTokensReq(s.cdc, s.ctx)). + Methods("GET") + r.HandleFunc(prefix+"/mini/tokens/{symbol}", s.handleMiniTokenReq(s.cdc, s.ctx)). + Methods("GET") + // fee params r.HandleFunc(prefix+"/fees", s.handleFeesParamReq(s.cdc, s.ctx)). Methods("GET") diff --git a/plugins/api/server.go b/plugins/api/server.go index a9f59876f..64c6a0810 100644 --- a/plugins/api/server.go +++ b/plugins/api/server.go @@ -5,10 +5,10 @@ import ( "github.com/cosmos/cosmos-sdk/client/context" keyscli "github.com/cosmos/cosmos-sdk/client/keys" - keys "github.com/cosmos/cosmos-sdk/crypto/keys" + "github.com/cosmos/cosmos-sdk/crypto/keys" "github.com/binance-chain/node/common" - tkstore "github.com/binance-chain/node/plugins/tokens/store" + "github.com/binance-chain/node/plugins/tokens" "github.com/binance-chain/node/wire" ) @@ -27,7 +27,7 @@ type server struct { // stores for handlers keyBase keys.Keybase - tokens tkstore.Mapper + tokens tokens.Mapper accStoreName string } @@ -45,7 +45,7 @@ func newServer(ctx context.CLIContext, cdc *wire.Codec) *server { ctx: ctx, cdc: cdc, keyBase: kb, - tokens: tkstore.NewMapper(cdc, common.TokenStoreKey), + tokens: tokens.NewMapper(cdc, common.TokenStoreKey), accStoreName: common.AccountStoreName, } } diff --git a/plugins/dex/abci.go b/plugins/dex/abci.go index ff4183d35..76bbc9173 100644 --- a/plugins/dex/abci.go +++ b/plugins/dex/abci.go @@ -9,6 +9,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" app "github.com/binance-chain/node/common/types" + "github.com/binance-chain/node/plugins/dex/order" "github.com/binance-chain/node/plugins/dex/store" "github.com/binance-chain/node/plugins/dex/types" "github.com/binance-chain/node/plugins/dex/utils" @@ -18,24 +19,25 @@ import ( const MaxDepthLevels = 1000 // matches UI requirement const DefaultDepthLevels = 100 // matches UI requirement -func createAbciQueryHandler(keeper *DexKeeper) app.AbciQueryHandler { +func createAbciQueryHandler(keeper *DexKeeper, abciQueryPrefix string) app.AbciQueryHandler { + queryPrefix := abciQueryPrefix return func(app app.ChainApp, req abci.RequestQuery, path []string) (res *abci.ResponseQuery) { // expects at least two query path segments. - if path[0] != AbciQueryPrefix || len(path) < 2 { + if path[0] != queryPrefix || len(path) < 2 { return nil } switch path[1] { - case "pairs": // args: ["dex", "pairs", , ] + case "pairs": // args: ["dex" or "dex-mini", "pairs", , ] if len(path) < 4 { return &abci.ResponseQuery{ Code: uint32(sdk.CodeUnknownRequest), Log: fmt.Sprintf( "%s %s query requires offset and limit in the path", - AbciQueryPrefix, path[1]), + queryPrefix, path[1]), } } ctx := app.GetContextForCheckState() - pairs := keeper.PairMapper.ListAllTradingPairs(ctx) + pairs := listPairs(keeper, ctx, queryPrefix) var offset, limit, end int var err error if pairs == nil || len(pairs) == 0 { @@ -81,6 +83,14 @@ func createAbciQueryHandler(keeper *DexKeeper) app.AbciQueryHandler { Value: bz, } case "orderbook": // args: ["dex", "orderbook"] + if queryPrefix == DexMiniAbciQueryPrefix { + return &abci.ResponseQuery{ + Code: uint32(sdk.ABCICodeOK), + Info: fmt.Sprintf( + "Unknown `%s` query path: %v", + queryPrefix, path), + } + } //TODO: sync lock, validate pair if len(path) < 3 { return &abci.ResponseQuery{ @@ -124,6 +134,14 @@ func createAbciQueryHandler(keeper *DexKeeper) app.AbciQueryHandler { Value: bz, } case "openorders": // args: ["dex", "openorders", , ] + if queryPrefix == DexMiniAbciQueryPrefix { + return &abci.ResponseQuery{ + Code: uint32(sdk.ABCICodeOK), + Info: fmt.Sprintf( + "Unknown `%s` query path: %v", + queryPrefix, path), + } + } if len(path) < 4 { return &abci.ResponseQuery{ Code: uint32(sdk.CodeUnknownRequest), @@ -141,8 +159,7 @@ func createAbciQueryHandler(keeper *DexKeeper) app.AbciQueryHandler { } } ctx := app.GetContextForCheckState() - existingPair, err := keeper.PairMapper.GetTradingPair(ctx, baseAsset, quoteAsset) - if pair != existingPair.GetSymbol() || err != nil { + if !keeper.PairMapper.Exists(ctx, baseAsset, quoteAsset) { return &abci.ResponseQuery{ Code: uint32(sdk.CodeInternal), Log: "pair is not listed", @@ -174,8 +191,25 @@ func createAbciQueryHandler(keeper *DexKeeper) app.AbciQueryHandler { Code: uint32(sdk.ABCICodeOK), Info: fmt.Sprintf( "Unknown `%s` query path: %v", - AbciQueryPrefix, path), + queryPrefix, path), } } } } + +func listPairs(keeper *DexKeeper, ctx sdk.Context, abciPrefix string) []types.TradingPair { + pairs := keeper.PairMapper.ListAllTradingPairs(ctx) + rs := make([]types.TradingPair, 0, len(pairs)) + for _, pair := range pairs { + if keeper.GetPairType(pair.GetSymbol()) == order.PairType.MINI { + if abciPrefix == DexMiniAbciQueryPrefix { + rs = append(rs, pair) + } + } else { + if abciPrefix == DexAbciQueryPrefix { + rs = append(rs, pair) + } + } + } + return rs +} \ No newline at end of file diff --git a/plugins/dex/aliases.go b/plugins/dex/aliases.go index 9e66f879d..9e7d34657 100644 --- a/plugins/dex/aliases.go +++ b/plugins/dex/aliases.go @@ -10,7 +10,9 @@ import ( // type TradingPair = types.TradingPair type TradingPairMapper = store.TradingPairMapper -type DexKeeper = order.Keeper +type DexKeeper = order.DexKeeper var NewTradingPairMapper = store.NewTradingPairMapper -var NewOrderKeeper = order.NewKeeper +var NewDexKeeper = order.NewDexKeeper + +const DefaultCodespace = types.DefaultCodespace diff --git a/plugins/dex/client/cli/commands.go b/plugins/dex/client/cli/commands.go index 30c3b1f3c..3ff94a2bf 100644 --- a/plugins/dex/client/cli/commands.go +++ b/plugins/dex/client/cli/commands.go @@ -22,6 +22,7 @@ func AddCommands(cmd *cobra.Command, cdc *wire.Codec) { dexCmd.AddCommand( client.PostCommands( listTradingPairCmd(cdc), + listMiniTradingPairCmd(cdc), client.LineBreak, newOrderCmd(cdc), cancelOrderCmd(cdc))...) diff --git a/plugins/dex/client/cli/list.go b/plugins/dex/client/cli/list.go index 864ebd668..9c515a8cd 100644 --- a/plugins/dex/client/cli/list.go +++ b/plugins/dex/client/cli/list.go @@ -32,13 +32,13 @@ func listTradingPairCmd(cdc *wire.Codec) *cobra.Command { } baseAsset := viper.GetString(flagBaseAsset) - err = types.ValidateMapperTokenSymbol(baseAsset) + err = types.ValidateTokenSymbol(baseAsset) if err != nil { return err } quoteAsset := viper.GetString(flagQuoteAsset) - err = types.ValidateMapperTokenSymbol(quoteAsset) + err = types.ValidateTokenSymbol(quoteAsset) if err != nil { return err } @@ -74,3 +74,52 @@ func listTradingPairCmd(cdc *wire.Codec) *cobra.Command { return cmd } + +func listMiniTradingPairCmd(cdc *wire.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "list-mini", + Short: "", + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx, txbldr := client.PrepareCtx(cdc) + + from, err := cliCtx.GetFromAddress() + if err != nil { + return err + } + + baseAsset := viper.GetString(flagBaseAsset) + err = types.ValidateMiniTokenSymbol(baseAsset) + if err != nil { + return err + } + + quoteAsset := viper.GetString(flagQuoteAsset) + if quoteAsset != types.NativeTokenSymbol && !strings.HasPrefix(quoteAsset, "BUSD") { + return errors.New("invalid quote asset") + } + + baseAsset = strings.ToUpper(baseAsset) + quoteAsset = strings.ToUpper(quoteAsset) + + initPriceStr := viper.GetString(flagInitPrice) + initPrice, err := utils.ParsePrice(initPriceStr) + if err != nil { + return err + } + + msg := list.NewListMiniMsg(from, baseAsset, quoteAsset, initPrice) + err = client.SendOrPrintTx(cliCtx, txbldr, msg) + if err != nil { + return err + } + + return nil + }, + } + + cmd.Flags().StringP(flagBaseAsset, "s", "", "symbol of the base asset") + cmd.Flags().String(flagQuoteAsset, "", "symbol of the quote currency") + cmd.Flags().String(flagInitPrice, "", "init price for this pair") + + return cmd +} diff --git a/plugins/dex/client/rest/getpairs.go b/plugins/dex/client/rest/getpairs.go index cb755a9c9..200551229 100644 --- a/plugins/dex/client/rest/getpairs.go +++ b/plugins/dex/client/rest/getpairs.go @@ -16,8 +16,8 @@ const maxPairsLimit = 1000 const defaultPairsLimit = 100 const defaultPairsOffset = 0 -func listAllTradingPairs(ctx context.CLIContext, cdc *wire.Codec, offset int, limit int) ([]types.TradingPair, error) { - bz, err := ctx.Query(fmt.Sprintf("dex/pairs/%d/%d", offset, limit), nil) +func listAllTradingPairs(ctx context.CLIContext, cdc *wire.Codec, prefix string, offset int, limit int) ([]types.TradingPair, error) { + bz, err := ctx.Query(fmt.Sprintf("%s/pairs/%d/%d", prefix, offset, limit), nil) if err != nil { return nil, err } @@ -27,7 +27,7 @@ func listAllTradingPairs(ctx context.CLIContext, cdc *wire.Codec, offset int, li } // GetPairsReqHandler creates an http request handler to list -func GetPairsReqHandler(cdc *wire.Codec, ctx context.CLIContext) http.HandlerFunc { +func GetPairsReqHandler(cdc *wire.Codec, ctx context.CLIContext, abciQueryPrefix string) http.HandlerFunc { type params struct { limit int offset int @@ -79,7 +79,7 @@ func GetPairsReqHandler(cdc *wire.Codec, ctx context.CLIContext) http.HandlerFun params.limit = maxPairsLimit } - pairs, err := listAllTradingPairs(ctx, cdc, params.offset, params.limit) + pairs, err := listAllTradingPairs(ctx, cdc, abciQueryPrefix, params.offset, params.limit) if err != nil { throw(w, http.StatusInternalServerError, err) return diff --git a/plugins/dex/list/handler.go b/plugins/dex/list/handler.go index 88e3bf896..7aced6b44 100644 --- a/plugins/dex/list/handler.go +++ b/plugins/dex/list/handler.go @@ -18,11 +18,13 @@ import ( ) // NewHandler initialises dex message handlers -func NewHandler(keeper *order.Keeper, tokenMapper tokens.Mapper, govKeeper gov.Keeper) sdk.Handler { +func NewHandler(keeper *order.DexKeeper, tokenMapper tokens.Mapper, govKeeper gov.Keeper) sdk.Handler { return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { switch msg := msg.(type) { case ListMsg: return handleList(ctx, keeper, tokenMapper, govKeeper, msg) + case ListMiniMsg: + return handleListMini(ctx, keeper, tokenMapper, msg) default: errMsg := fmt.Sprintf("Unrecognized dex msg type: %v", reflect.TypeOf(msg).Name()) return sdk.ErrUnknownRequest(errMsg).Result() @@ -74,7 +76,7 @@ func checkListProposal(ctx sdk.Context, govKeeper gov.Keeper, msg ListMsg) error return nil } -func handleList(ctx sdk.Context, keeper *order.Keeper, tokenMapper tokens.Mapper, govKeeper gov.Keeper, +func handleList(ctx sdk.Context, keeper *order.DexKeeper, tokenMapper tokens.Mapper, govKeeper gov.Keeper, msg ListMsg) sdk.Result { if err := checkListProposal(ctx, govKeeper, msg); err != nil { return types.ErrInvalidProposal(err.Error()).Result() @@ -103,12 +105,12 @@ func handleList(ctx sdk.Context, keeper *order.Keeper, tokenMapper tokens.Mapper return sdk.ErrUnauthorized("only the owner of the token can list the token").Result() } - if !tokenMapper.Exists(ctx, msg.QuoteAssetSymbol) { + if !tokenMapper.ExistsBEP2(ctx, msg.QuoteAssetSymbol) { return sdk.ErrInvalidCoins("quote token does not exist").Result() } } - if !tokenMapper.Exists(ctx, msg.QuoteAssetSymbol) { + if !tokenMapper.ExistsBEP2(ctx, msg.QuoteAssetSymbol) { return sdk.ErrInvalidCoins("quote token does not exist").Result() } diff --git a/plugins/dex/list/handler_mini.go b/plugins/dex/list/handler_mini.go new file mode 100644 index 000000000..317cf6063 --- /dev/null +++ b/plugins/dex/list/handler_mini.go @@ -0,0 +1,46 @@ +package list + +import ( + "github.com/binance-chain/node/common/log" + "github.com/binance-chain/node/plugins/dex/order" + "github.com/binance-chain/node/plugins/dex/types" + "github.com/binance-chain/node/plugins/tokens" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func handleListMini(ctx sdk.Context, dexKeeper *order.DexKeeper, tokenMapper tokens.Mapper, + msg ListMiniMsg) sdk.Result { + + if err := dexKeeper.CanListTradingPair(ctx, msg.BaseAssetSymbol, msg.QuoteAssetSymbol); err != nil { + return sdk.ErrInvalidCoins(err.Error()).Result() + } + + baseToken, err := tokenMapper.GetToken(ctx, msg.BaseAssetSymbol) + if err != nil { + return sdk.ErrInvalidCoins(err.Error()).Result() + } + + quoteToken, err := tokenMapper.GetToken(ctx, msg.QuoteAssetSymbol) + if err != nil { + return sdk.ErrInvalidCoins(err.Error()).Result() + } + + if !baseToken.IsOwner(msg.From) && !quoteToken.IsOwner(msg.From) { + return sdk.ErrUnauthorized("only the owner of the base asset or quote asset can list the trading pair").Result() + } + + lotSize := dexKeeper.DetermineLotSize(msg.BaseAssetSymbol, msg.QuoteAssetSymbol, msg.InitPrice) + pair := types.NewTradingPairWithLotSize(msg.BaseAssetSymbol, msg.QuoteAssetSymbol, msg.InitPrice, lotSize) + err = dexKeeper.PairMapper.AddTradingPair(ctx, pair) + if err != nil { + return sdk.ErrInternal(err.Error()).Result() + } + + // this is done in memory! we must not run this block in checktx or simulate! + if ctx.IsDeliverTx() { // only add engine during DeliverTx + dexKeeper.AddEngine(pair) + log.With("module", "dex").Info("List new mini-token Pair and created new match engine", "pair", pair) + } + + return sdk.Result{} +} diff --git a/plugins/dex/list/handler_mini_test.go b/plugins/dex/list/handler_mini_test.go new file mode 100644 index 000000000..99b83d253 --- /dev/null +++ b/plugins/dex/list/handler_mini_test.go @@ -0,0 +1,118 @@ +package list + +import ( + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + + "github.com/binance-chain/node/common/types" + "github.com/binance-chain/node/common/upgrade" + "github.com/binance-chain/node/plugins/tokens" +) + +func setChainVersion() { + upgrade.Mgr.AddUpgradeHeight(upgrade.BEP8, -1) + upgrade.Mgr.AddUpgradeHeight(upgrade.BEP70, -1) +} + +func resetChainVersion() { + upgrade.Mgr.Config.HeightMap = nil +} + +func setupForMini(ctx sdk.Context, tokenMapper tokens.Mapper, t *testing.T) { + err := tokenMapper.NewToken(ctx, &types.Token{ + Name: "Bitcoin", + Symbol: "BTC-000", + OrigSymbol: "BTC", + TotalSupply: 10000, + Owner: sdk.AccAddress("testacc"), + }) + require.Nil(t, err, "new token error") + + err = tokenMapper.NewToken(ctx, &types.Token{ + Name: "Native Token", + Symbol: types.NativeTokenSymbol, + OrigSymbol: types.NativeTokenSymbol, + TotalSupply: 10000, + Owner: sdk.AccAddress("testacc"), + }) + require.Nil(t, err, "new token error") + + miniToken := types.NewMiniToken("Bitcoin Mini", "BTC", "BTC-000M", types.MiniRangeType, 100000e8, sdk.AccAddress("testacc"), false, "") + err = tokenMapper.NewToken(ctx, miniToken) + require.Nil(t, err, "new token error") + + tinyToken := types.NewMiniToken("Bitcoin Mini", "ETH", "ETH-000M", types.TinyRangeType, 10000e8, sdk.AccAddress("testacc"), true, "abc") + err = tokenMapper.NewToken(ctx, tinyToken) + require.Nil(t, err, "new token error") +} + +func TestHandleListMiniIdenticalSymbols(t *testing.T) { + setChainVersion() + defer resetChainVersion() + cdc := MakeCodec() + ms, orderKeeper, tokenMapper, _ := MakeKeepers(cdc) + ctx := sdk.NewContext(ms, abci.Header{}, sdk.RunTxModeDeliver, log.NewNopLogger()) + setupForMini(ctx, tokenMapper, t) + result := handleListMini(ctx, orderKeeper, tokenMapper, ListMiniMsg{ + From: sdk.AccAddress("testacc"), + BaseAssetSymbol: "BTC-000M", + QuoteAssetSymbol: "BTC-000M", + InitPrice: 1000, + }) + require.Contains(t, result.Log, "base asset symbol should not be identical to quote asset symbol") +} + +func TestHandleListMiniWrongBaseSymbol(t *testing.T) { + setChainVersion() + defer resetChainVersion() + cdc := MakeCodec() + ms, orderKeeper, tokenMapper, _ := MakeKeepers(cdc) + ctx := sdk.NewContext(ms, abci.Header{}, sdk.RunTxModeDeliver, log.NewNopLogger()) + setupForMini(ctx, tokenMapper, t) + result := handleListMini(ctx, orderKeeper, tokenMapper, ListMiniMsg{ + From: sdk.AccAddress("testacc"), + BaseAssetSymbol: "BTC", + QuoteAssetSymbol: "BNB", + InitPrice: 1000, + }) + //require.Equal(t, result.Code, sdk.ABCICodeOK) + require.Contains(t, result.Log, "token(BTC) not found") +} + +func TestHandleListMiniRight(t *testing.T) { + setChainVersion() + defer resetChainVersion() + cdc := MakeCodec() + ms, orderKeeper, tokenMapper, _ := MakeKeepers(cdc) + ctx := sdk.NewContext(ms, abci.Header{}, sdk.RunTxModeDeliver, log.NewNopLogger()) + setupForMini(ctx, tokenMapper, t) + result := handleListMini(ctx, orderKeeper, tokenMapper, ListMiniMsg{ + From: sdk.AccAddress("testacc"), + BaseAssetSymbol: "BTC-000M", + QuoteAssetSymbol: "BNB", + InitPrice: 1000, + }) + require.Equal(t, result.Code, sdk.ABCICodeOK) +} + +func TestHandleListTinyRight(t *testing.T) { + setChainVersion() + defer resetChainVersion() + cdc := MakeCodec() + ms, orderKeeper, tokenMapper, _ := MakeKeepers(cdc) + ctx := sdk.NewContext(ms, abci.Header{}, sdk.RunTxModeDeliver, log.NewNopLogger()) + setupForMini(ctx, tokenMapper, t) + result := handleListMini(ctx, orderKeeper, tokenMapper, ListMiniMsg{ + From: sdk.AccAddress("testacc"), + BaseAssetSymbol: "ETH-000M", + QuoteAssetSymbol: "BNB", + InitPrice: 1000, + }) + require.Equal(t, result.Code, sdk.ABCICodeOK) +} diff --git a/plugins/dex/list/handler_test.go b/plugins/dex/list/handler_test.go index e161fa56d..aec312a94 100644 --- a/plugins/dex/list/handler_test.go +++ b/plugins/dex/list/handler_test.go @@ -27,7 +27,6 @@ import ( "github.com/binance-chain/node/plugins/dex/store" dexTypes "github.com/binance-chain/node/plugins/dex/types" "github.com/binance-chain/node/plugins/tokens" - tokenStore "github.com/binance-chain/node/plugins/tokens/store" ) func MakeCodec() *codec.Codec { @@ -42,7 +41,7 @@ func MakeCodec() *codec.Codec { return cdc } -func MakeKeepers(cdc *codec.Codec) (ms sdkStore.CommitMultiStore, orderKeeper *order.Keeper, tokenMapper tokenStore.Mapper, govKeeper gov.Keeper) { +func MakeKeepers(cdc *codec.Codec) (ms sdkStore.CommitMultiStore, dexKeeper *order.DexKeeper, tokenMapper tokens.Mapper, govKeeper gov.Keeper) { accKey := sdk.NewKVStoreKey("acc") pairKey := sdk.NewKVStoreKey("pair") tokenKey := sdk.NewKVStoreKey("token") @@ -65,10 +64,9 @@ func MakeKeepers(cdc *codec.Codec) (ms sdkStore.CommitMultiStore, orderKeeper *o accKeeper := auth.NewAccountKeeper(cdc, accKey, types.ProtoAppAccount) codespacer := sdk.NewCodespacer() pairMapper := store.NewTradingPairMapper(cdc, pairKey) - orderKeeper = order.NewKeeper(common.DexStoreKey, accKeeper, pairMapper, - codespacer.RegisterNext(dexTypes.DefaultCodespace), 2, cdc, false) + dexKeeper = order.NewDexKeeper(common.DexStoreKey, accKeeper, pairMapper, codespacer.RegisterNext(dexTypes.DefaultCodespace), 2, cdc, false) - tokenMapper = tokenStore.NewMapper(cdc, tokenKey) + tokenMapper = tokens.NewMapper(cdc, tokenKey) paramsKeeper := params.NewKeeper(cdc, paramKey, paramTKey) bankKeeper := bank.NewBaseKeeper(accKeeper) @@ -85,7 +83,7 @@ func MakeKeepers(cdc *codec.Codec) (ms sdkStore.CommitMultiStore, orderKeeper *o gov.DefaultCodespace, new(sdk.Pool)) - return ms, orderKeeper, tokenMapper, govKeeper + return ms, dexKeeper, tokenMapper, govKeeper } func getProposal(lowerCase bool, baseAssetSymbol string, quoteAssetSymbol string) gov.Proposal { @@ -207,7 +205,7 @@ func TestListHandler(t *testing.T) { }) require.Contains(t, result.Log, "token(BTC-000) not found") - err := tokenMapper.NewToken(ctx, types.Token{ + err := tokenMapper.NewToken(ctx, &types.Token{ Name: "Bitcoin", Symbol: "BTC-000", OrigSymbol: "BTC", @@ -234,7 +232,7 @@ func TestListHandler(t *testing.T) { }) require.Contains(t, result.Log, "quote token does not exist") - err = tokenMapper.NewToken(ctx, types.Token{ + err = tokenMapper.NewToken(ctx, &types.Token{ Name: "Native Token", Symbol: types.NativeTokenSymbol, OrigSymbol: types.NativeTokenSymbol, @@ -258,7 +256,7 @@ func TestListHandler_LowerCase(t *testing.T) { cdc := MakeCodec() ms, orderKeeper, tokenMapper, govKeeper := MakeKeepers(cdc) ctx := sdk.NewContext(ms, abci.Header{}, sdk.RunTxModeDeliver, log.NewNopLogger()) - err := tokenMapper.NewToken(ctx, types.Token{ + err := tokenMapper.NewToken(ctx, &types.Token{ Name: "Bitcoin", Symbol: "BTC-000", OrigSymbol: "BTC", @@ -267,7 +265,7 @@ func TestListHandler_LowerCase(t *testing.T) { }) require.Nil(t, err, "new token error") - err = tokenMapper.NewToken(ctx, types.Token{ + err = tokenMapper.NewToken(ctx, &types.Token{ Name: "Native Token", Symbol: types.NativeTokenSymbol, OrigSymbol: types.NativeTokenSymbol, @@ -325,7 +323,7 @@ func TestListHandler_AfterUpgrade(t *testing.T) { cdc := MakeCodec() ms, orderKeeper, tokenMapper, govKeeper := MakeKeepers(cdc) ctx := sdk.NewContext(ms, abci.Header{}, sdk.RunTxModeDeliver, log.NewNopLogger()) - err := tokenMapper.NewToken(ctx, types.Token{ + err := tokenMapper.NewToken(ctx, &types.Token{ Name: "Bitcoin", Symbol: "BTC-000", OrigSymbol: "BTC", @@ -334,7 +332,7 @@ func TestListHandler_AfterUpgrade(t *testing.T) { }) require.Nil(t, err, "new token error") - err = tokenMapper.NewToken(ctx, types.Token{ + err = tokenMapper.NewToken(ctx, &types.Token{ Name: "Native Token", Symbol: types.NativeTokenSymbol, OrigSymbol: types.NativeTokenSymbol, diff --git a/plugins/dex/list/hooks.go b/plugins/dex/list/hooks.go index 89fd5302e..bce51f7f7 100644 --- a/plugins/dex/list/hooks.go +++ b/plugins/dex/list/hooks.go @@ -14,11 +14,11 @@ import ( ) type ListHooks struct { - orderKeeper *order.Keeper + orderKeeper *order.DexKeeper tokenMapper tokens.Mapper } -func NewListHooks(orderKeeper *order.Keeper, tokenMapper tokens.Mapper) ListHooks { +func NewListHooks(orderKeeper *order.DexKeeper, tokenMapper tokens.Mapper) ListHooks { return ListHooks{ orderKeeper: orderKeeper, tokenMapper: tokenMapper, @@ -58,11 +58,11 @@ func (hooks ListHooks) OnProposalSubmitted(ctx sdk.Context, proposal gov.Proposa return errors.New("expire time should after now") } - if !hooks.tokenMapper.Exists(ctx, listParams.BaseAssetSymbol) { + if !hooks.tokenMapper.ExistsBEP2(ctx, listParams.BaseAssetSymbol) { return errors.New("base token does not exist") } - if !hooks.tokenMapper.Exists(ctx, listParams.QuoteAssetSymbol) { + if !hooks.tokenMapper.ExistsBEP2(ctx, listParams.QuoteAssetSymbol) { return errors.New("quote token does not exist") } @@ -74,10 +74,10 @@ func (hooks ListHooks) OnProposalSubmitted(ctx sdk.Context, proposal gov.Proposa } type DelistHooks struct { - orderKeeper *order.Keeper + orderKeeper *order.DexKeeper } -func NewDelistHooks(orderKeeper *order.Keeper) DelistHooks { +func NewDelistHooks(orderKeeper *order.DexKeeper) DelistHooks { return DelistHooks{ orderKeeper: orderKeeper, } diff --git a/plugins/dex/list/hooks_test.go b/plugins/dex/list/hooks_test.go index 2c86f610f..996a0f946 100644 --- a/plugins/dex/list/hooks_test.go +++ b/plugins/dex/list/hooks_test.go @@ -177,7 +177,7 @@ func TestTradingPairExists(t *testing.T) { ctx := sdk.NewContext(ms, abci.Header{}, sdk.RunTxModeDeliver, log.NewNopLogger()) - err = tokenMapper.NewToken(ctx, types.Token{ + err = tokenMapper.NewToken(ctx, &types.Token{ Name: "Native Token", Symbol: listParams.BaseAssetSymbol, OrigSymbol: listParams.BaseAssetSymbol, @@ -186,7 +186,7 @@ func TestTradingPairExists(t *testing.T) { }) require.Nil(t, err, "new token error") - err = tokenMapper.NewToken(ctx, types.Token{ + err = tokenMapper.NewToken(ctx, &types.Token{ Name: "Native Token", Symbol: listParams.QuoteAssetSymbol, OrigSymbol: "BTC", @@ -226,7 +226,7 @@ func TestPrerequisiteTradingPair(t *testing.T) { ctx := sdk.NewContext(ms, abci.Header{}, sdk.RunTxModeDeliver, log.NewNopLogger()) - err = tokenMapper.NewToken(ctx, types.Token{ + err = tokenMapper.NewToken(ctx, &types.Token{ Name: "Native Token", Symbol: listParams.BaseAssetSymbol, OrigSymbol: "BTC", @@ -235,7 +235,7 @@ func TestPrerequisiteTradingPair(t *testing.T) { }) require.Nil(t, err, "new token error") - err = tokenMapper.NewToken(ctx, types.Token{ + err = tokenMapper.NewToken(ctx, &types.Token{ Name: "Native Token", Symbol: listParams.QuoteAssetSymbol, OrigSymbol: "ETH", @@ -260,7 +260,7 @@ func TestPrerequisiteTradingPair(t *testing.T) { err = orderKeeper.PairMapper.AddTradingPair(ctx, pair) require.Nil(t, err, "add trading pair error") - err = tokenMapper.NewToken(ctx, types.Token{ + err = tokenMapper.NewToken(ctx, &types.Token{ Name: "Native Token", Symbol: listParams.BaseAssetSymbol, OrigSymbol: "BTC", @@ -269,7 +269,7 @@ func TestPrerequisiteTradingPair(t *testing.T) { }) require.Nil(t, err, "new token error") - err = tokenMapper.NewToken(ctx, types.Token{ + err = tokenMapper.NewToken(ctx, &types.Token{ Name: "Native Token", Symbol: listParams.QuoteAssetSymbol, OrigSymbol: "ETH", @@ -330,7 +330,7 @@ func TestQuoteTokenDoesNotExist(t *testing.T) { ctx := sdk.NewContext(ms, abci.Header{}, sdk.RunTxModeDeliver, log.NewNopLogger()) - err = tokenMapper.NewToken(ctx, types.Token{ + err = tokenMapper.NewToken(ctx, &types.Token{ Name: "Native Token", Symbol: listParams.BaseAssetSymbol, OrigSymbol: "BNB", @@ -366,7 +366,7 @@ func TestRightProposal(t *testing.T) { ctx := sdk.NewContext(ms, abci.Header{}, sdk.RunTxModeDeliver, log.NewNopLogger()) - err = tokenMapper.NewToken(ctx, types.Token{ + err = tokenMapper.NewToken(ctx, &types.Token{ Name: "Native Token", Symbol: listParams.BaseAssetSymbol, OrigSymbol: listParams.BaseAssetSymbol, @@ -375,7 +375,7 @@ func TestRightProposal(t *testing.T) { }) require.Nil(t, err, "new token error") - err = tokenMapper.NewToken(ctx, types.Token{ + err = tokenMapper.NewToken(ctx, &types.Token{ Name: "Native Token", Symbol: listParams.QuoteAssetSymbol, OrigSymbol: "BTC", diff --git a/plugins/dex/list/msg.go b/plugins/dex/list/msg.go index 286687846..9845366c7 100644 --- a/plugins/dex/list/msg.go +++ b/plugins/dex/list/msg.go @@ -40,11 +40,11 @@ func (msg ListMsg) ValidateBasic() sdk.Error { if msg.ProposalId <= 0 { return sdk.ErrInvalidCoins("proposal id should be positive") } - err := types.ValidateMapperTokenSymbol(msg.BaseAssetSymbol) + err := types.ValidateTokenSymbol(msg.BaseAssetSymbol) if err != nil { return sdk.ErrInvalidCoins("base token: " + err.Error()) } - err = types.ValidateMapperTokenSymbol(msg.QuoteAssetSymbol) + err = types.ValidateTokenSymbol(msg.QuoteAssetSymbol) if err != nil { return sdk.ErrInvalidCoins("quote token: " + err.Error()) } diff --git a/plugins/dex/list/msg_mini.go b/plugins/dex/list/msg_mini.go new file mode 100644 index 000000000..09c0111ba --- /dev/null +++ b/plugins/dex/list/msg_mini.go @@ -0,0 +1,75 @@ +package list + +import ( + "encoding/json" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/binance-chain/node/common/types" + "github.com/binance-chain/node/common/upgrade" + "github.com/binance-chain/node/plugins/dex/order" +) + +const MiniMsg = "dexListMini" + +var _ sdk.Msg = ListMiniMsg{} + +type ListMiniMsg struct { + From sdk.AccAddress `json:"from"` + BaseAssetSymbol string `json:"base_asset_symbol"` + QuoteAssetSymbol string `json:"quote_asset_symbol"` + InitPrice int64 `json:"init_price"` +} + +func NewListMiniMsg(from sdk.AccAddress, baseAssetSymbol string, quoteAssetSymbol string, initPrice int64) ListMiniMsg { + return ListMiniMsg{ + From: from, + BaseAssetSymbol: baseAssetSymbol, + QuoteAssetSymbol: quoteAssetSymbol, + InitPrice: initPrice, + } +} + +func (msg ListMiniMsg) Route() string { return Route } +func (msg ListMiniMsg) Type() string { return MiniMsg } +func (msg ListMiniMsg) String() string { return fmt.Sprintf("MsgListMini{%#v}", msg) } +func (msg ListMiniMsg) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.From} } + +func (msg ListMiniMsg) ValidateBasic() sdk.Error { + err := types.ValidateMiniTokenSymbol(msg.BaseAssetSymbol) + if err != nil { + return sdk.ErrInvalidCoins("base token: " + err.Error()) + } + if len(msg.QuoteAssetSymbol) == 0 { + return sdk.ErrInvalidCoins("quote token is empty ") + } + + // before BEP70 upgraded, we only support listing mini token against NativeToken + if sdk.IsUpgrade(upgrade.BEP70) { + if types.NativeTokenSymbol != msg.QuoteAssetSymbol && order.BUSDSymbol != msg.QuoteAssetSymbol { + return sdk.ErrInvalidCoins("quote token is not valid ") + } + } else { + if types.NativeTokenSymbol != msg.QuoteAssetSymbol { + return sdk.ErrInvalidCoins("quote token is not valid ") + } + } + + if msg.InitPrice <= 0 { + return sdk.ErrInvalidCoins("price should be positive") + } + return nil +} + +func (msg ListMiniMsg) GetSignBytes() []byte { + b, err := json.Marshal(msg) + if err != nil { + panic(err) + } + return b +} + +func (msg ListMiniMsg) GetInvolvedAddresses() []sdk.AccAddress { + return msg.GetSigners() +} diff --git a/plugins/dex/list/msg_mini_test.go b/plugins/dex/list/msg_mini_test.go new file mode 100644 index 000000000..b399247c3 --- /dev/null +++ b/plugins/dex/list/msg_mini_test.go @@ -0,0 +1,67 @@ +package list + +import ( + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/binance-chain/node/plugins/dex/order" +) + +func TestMiniIdenticalBaseAssetAndQuoteAsset(t *testing.T) { + msg := NewListMiniMsg(sdk.AccAddress{}, "BTC-000M", "BTC-000M", 1000) + err := msg.ValidateBasic() + require.NotNil(t, err, "msg should be error") + require.Contains(t, err.Error(), "quote token is not valid") +} + +func TestMiniWrongBaseAssetSymbol(t *testing.T) { + msg := NewListMiniMsg(sdk.AccAddress{}, "BTC", "BTC-000", 1000) + err := msg.ValidateBasic() + require.NotNil(t, err, "msg should be error") + require.Contains(t, err.Error(), "base token: suffixed token symbol must contain a hyphen ('-')") +} + +func TestMiniWrongBaseAssetSymbolNotMiniToken(t *testing.T) { + msg := NewListMiniMsg(sdk.AccAddress{}, "BTC-000", "BTC-000", 1000) + err := msg.ValidateBasic() + require.NotNil(t, err, "msg should be error") + require.Contains(t, err.Error(), "base token: mini-token symbol suffix must be 4 chars in length, got 3") +} + +func TestMiniWrongQuoteAssetSymbol(t *testing.T) { + msg := NewListMiniMsg(sdk.AccAddress{}, "BTC-000M", "ETH-123", 1000) + err := msg.ValidateBasic() + require.NotNil(t, err, "msg should be error") + require.Contains(t, err.Error(), "quote token is not valid") +} + +func TestMiniWrongInitPrice(t *testing.T) { + msg := NewListMiniMsg(sdk.AccAddress{}, "BTC-000M", "BNB", -1000) + err := msg.ValidateBasic() + require.NotNil(t, err, "msg should be error") + require.Contains(t, err.Error(), "price should be positive") +} + +func TestMiniRightMsg(t *testing.T) { + msg := NewListMiniMsg(sdk.AccAddress{}, "BTC-000M", "BNB", 1000) + err := msg.ValidateBasic() + require.Nil(t, err, "msg should not be error") +} + +func TestMiniBUSDQuote(t *testing.T) { + msg := NewListMiniMsg(sdk.AccAddress{}, "BTC-000M", "BUSD-000", 1000) + err := msg.ValidateBasic() + require.NotNil(t, err, "msg should be error") + require.Contains(t, err.Error(), "quote token is not valid") + + setChainVersion() + defer resetChainVersion() + order.BUSDSymbol = "BUSD-000" + msg = NewListMiniMsg(sdk.AccAddress{}, "BTC-000M", "BUSD-000", 1000) + err = msg.ValidateBasic() + require.Nil(t, err, "msg should not be error") + +} diff --git a/plugins/dex/matcheng/engine_new.go b/plugins/dex/matcheng/engine_new.go index 8070382b7..937511a78 100644 --- a/plugins/dex/matcheng/engine_new.go +++ b/plugins/dex/matcheng/engine_new.go @@ -13,6 +13,13 @@ import ( ) func (me *MatchEng) Match(height int64) bool { + success := me.runMatch(height) + if sdk.IsUpgrade(upgrade.BEP19) { + me.LastMatchHeight = height + } + return success +} +func (me *MatchEng) runMatch(height int64) bool { if !sdk.IsUpgrade(upgrade.BEP19) { return me.MatchBeforeGalileo(height) } @@ -32,7 +39,8 @@ func (me *MatchEng) Match(height int64) bool { me.logger.Error("dropRedundantQty failed", "error", err) return false } - takerSide, err := me.determineTakerSide(height, index) + //If order height > the last Match height, then it's maker. + takerSide, err := me.determineTakerSide(index) if err != nil { me.logger.Error("determineTakerSide failed", "error", err) return false @@ -41,7 +49,6 @@ func (me *MatchEng) Match(height int64) bool { surplus := me.overLappedLevel[index].BuySellSurplus me.fillOrdersNew(takerSide, takerSideOrders, index, tradePrice, surplus) me.LastTradePrice = tradePrice - me.LastMatchHeight = height return true } @@ -115,23 +122,23 @@ func dropRedundantQty(orders []OrderPart, toDropQty, lotSize int64) error { return nil } -func findTakerStartIdx(height int64, orders []OrderPart) (idx int, makerTotal int64) { +func findTakerStartIdx(lastMatchHeight int64, orders []OrderPart) (idx int, makerTotal int64) { i, k := 0, len(orders) for ; i < k; i++ { - if orders[i].Time >= height { - return i, makerTotal - } else { + if orders[i].Time <= lastMatchHeight { makerTotal += orders[i].nxtTrade + } else { + return i, makerTotal } } return i, makerTotal } -func (me *MatchEng) determineTakerSide(height int64, tradePriceIdx int) (int8, error) { +func (me *MatchEng) determineTakerSide(tradePriceIdx int) (int8, error) { makerSide := UNKNOWN for i := 0; i <= tradePriceIdx; i++ { l := &me.overLappedLevel[i] - l.BuyTakerStartIdx, l.BuyMakerTotal = findTakerStartIdx(height, l.BuyOrders) + l.BuyTakerStartIdx, l.BuyMakerTotal = findTakerStartIdx(me.LastMatchHeight, l.BuyOrders) if l.HasBuyMaker() { makerSide = BUYSIDE } @@ -139,7 +146,7 @@ func (me *MatchEng) determineTakerSide(height int64, tradePriceIdx int) (int8, e for i := len(me.overLappedLevel) - 1; i >= tradePriceIdx; i-- { l := &me.overLappedLevel[i] - l.SellTakerStartIdx, l.SellMakerTotal = findTakerStartIdx(height, l.SellOrders) + l.SellTakerStartIdx, l.SellMakerTotal = findTakerStartIdx(me.LastMatchHeight, l.SellOrders) if l.HasSellMaker() { if makerSide == BUYSIDE { return UNKNOWN, errors.New("both buy side and sell side have maker orders.") diff --git a/plugins/dex/matcheng/match_new_test.go b/plugins/dex/matcheng/match_new_test.go index 82b849cd8..4198fab33 100644 --- a/plugins/dex/matcheng/match_new_test.go +++ b/plugins/dex/matcheng/match_new_test.go @@ -485,8 +485,8 @@ func TestMatchEng_determineTakerSide(t *testing.T) { assert.Equal(sellMakerTotal, l.SellMakerTotal) l.BuyTakerStartIdx, l.BuyMakerTotal, l.SellTakerStartIdx, l.SellMakerTotal = 0, 0, 0, 0 } - - takerSide, err := me.determineTakerSide(100, 0) + me.LastMatchHeight = 99 + takerSide, err := me.determineTakerSide(0) assert.NoError(err) assert.Equal(BUYSIDE, takerSide) checkAndClear(&me.overLappedLevel[0], 0, 0, 2, 200) @@ -494,7 +494,7 @@ func TestMatchEng_determineTakerSide(t *testing.T) { checkAndClear(&me.overLappedLevel[2], 0, 0, 0, 0) checkAndClear(&me.overLappedLevel[3], 0, 0, 0, 0) - takerSide, err = me.determineTakerSide(100, 1) + takerSide, err = me.determineTakerSide(1) assert.NoError(err) assert.Equal(BUYSIDE, takerSide) checkAndClear(&me.overLappedLevel[0], 0, 0, 0, 0) @@ -502,7 +502,7 @@ func TestMatchEng_determineTakerSide(t *testing.T) { checkAndClear(&me.overLappedLevel[2], 0, 0, 0, 0) checkAndClear(&me.overLappedLevel[3], 0, 0, 0, 0) - takerSide, err = me.determineTakerSide(100, 2) + takerSide, err = me.determineTakerSide(2) assert.NoError(err) assert.Equal(SELLSIDE, takerSide) checkAndClear(&me.overLappedLevel[0], 0, 0, 0, 0) @@ -510,7 +510,7 @@ func TestMatchEng_determineTakerSide(t *testing.T) { checkAndClear(&me.overLappedLevel[2], 2, 200, 0, 0) checkAndClear(&me.overLappedLevel[3], 0, 0, 0, 0) - takerSide, err = me.determineTakerSide(100, 3) + takerSide, err = me.determineTakerSide(3) assert.NoError(err) assert.Equal(SELLSIDE, takerSide) checkAndClear(&me.overLappedLevel[0], 0, 0, 0, 0) @@ -527,7 +527,7 @@ func TestMatchEng_determineTakerSide(t *testing.T) { {"2", 100, 100, 0, 100}, }, }} - takerSide, err = me.determineTakerSide(100, 0) + takerSide, err = me.determineTakerSide(0) assert.NoError(err) assert.Equal(BUYSIDE, takerSide) checkAndClear(&me.overLappedLevel[0], 0, 0, 0, 0) @@ -541,7 +541,7 @@ func TestMatchEng_determineTakerSide(t *testing.T) { {"2", 99, 100, 0, 100}, }, }} - takerSide, err = me.determineTakerSide(100, 0) + takerSide, err = me.determineTakerSide(0) assert.EqualError(err, "both buy side and sell side have maker orders.") assert.Equal(UNKNOWN, takerSide) } @@ -923,6 +923,7 @@ func TestMatchEng_Match(t *testing.T) { me.Book.InsertOrder("16", BUYSIDE, 100, 100, 20) upgrade.Mgr.SetHeight(100) + me.LastMatchHeight = 99 assert.True(me.Match(100)) assert.Equal(4, len(me.overLappedLevel)) assert.Equal(int64(100), me.LastTradePrice) @@ -978,7 +979,7 @@ func TestMatchEng_Match(t *testing.T) { me.Book.InsertOrder("15", SELLSIDE, 100, 100, 30) me.Book.InsertOrder("6", BUYSIDE, 100, 110, 40) me.Book.InsertOrder("8", BUYSIDE, 100, 110, 100) - + me.LastMatchHeight = 99 assert.True(me.Match(100)) assert.Equal(4, len(me.overLappedLevel)) assert.Equal(int64(104), me.LastTradePrice) diff --git a/plugins/dex/order/fee.go b/plugins/dex/order/fee.go index 5979ed686..6657ea8f6 100644 --- a/plugins/dex/order/fee.go +++ b/plugins/dex/order/fee.go @@ -52,7 +52,7 @@ type FeeManager struct { FeeConfig FeeConfig } -func NewFeeManager(cdc *wire.Codec, storeKey sdk.StoreKey, logger tmlog.Logger) *FeeManager { +func NewFeeManager(cdc *wire.Codec, logger tmlog.Logger) *FeeManager { return &FeeManager{ cdc: cdc, logger: logger, diff --git a/plugins/dex/order/fee_test.go b/plugins/dex/order/fee_test.go index 4e40b26e6..903bd89ea 100644 --- a/plugins/dex/order/fee_test.go +++ b/plugins/dex/order/fee_test.go @@ -3,8 +3,6 @@ package order import ( "testing" - "github.com/binance-chain/node/common/upgrade" - "github.com/stretchr/testify/require" sdk "github.com/cosmos/cosmos-sdk/types" @@ -28,34 +26,34 @@ func NewTestFeeConfig() FeeConfig { return feeConfig } -func TestFeeManager_calcTradeFeeForSingleTransfer(t *testing.T) { +func feeManagerCalcTradeFeeForSingleTransfer(t *testing.T, symbol string) { ctx, am, keeper := setup() keeper.FeeManager.UpdateConfig(NewTestFeeConfig()) - keeper.AddEngine(dextype.NewTradingPair("ABC-000", "BNB", 1e7)) + keeper.AddEngine(dextype.NewTradingPair(symbol, "BNB", 1e7)) keeper.AddEngine(dextype.NewTradingPair("BNB", "XYZ-111", 1e7)) _, acc := testutils.NewAccount(ctx, am, 0) tran := Transfer{ - inAsset: "ABC-000", + inAsset: symbol, in: 1000, outAsset: "BNB", out: 100, } // no enough bnb or native fee rounding to 0 fee := keeper.FeeManager.calcTradeFeeFromTransfer(acc.GetCoins(), &tran, keeper.engines) - require.Equal(t, sdk.Coins{{"ABC-000", 1}}, fee.Tokens) + require.Equal(t, sdk.Coins{{symbol, 1}}, fee.Tokens) _, acc = testutils.NewAccount(ctx, am, 100) fee = keeper.FeeManager.calcTradeFeeFromTransfer(acc.GetCoins(), &tran, keeper.engines) - require.Equal(t, sdk.Coins{{"ABC-000", 1}}, fee.Tokens) + require.Equal(t, sdk.Coins{{symbol, 1}}, fee.Tokens) tran = Transfer{ - inAsset: "ABC-000", + inAsset: symbol, in: 1000000, outAsset: "BNB", out: 10000, } _, acc = testutils.NewAccount(ctx, am, 1) fee = keeper.FeeManager.calcTradeFeeFromTransfer(acc.GetCoins(), &tran, keeper.engines) - require.Equal(t, sdk.Coins{{"ABC-000", 1000}}, fee.Tokens) + require.Equal(t, sdk.Coins{{symbol, 1000}}, fee.Tokens) _, acc = testutils.NewAccount(ctx, am, 100) fee = keeper.FeeManager.calcTradeFeeFromTransfer(acc.GetCoins(), &tran, keeper.engines) require.Equal(t, sdk.Coins{{"BNB", 5}}, fee.Tokens) @@ -63,7 +61,7 @@ func TestFeeManager_calcTradeFeeForSingleTransfer(t *testing.T) { tran = Transfer{ inAsset: "BNB", in: 100, - outAsset: "ABC-000", + outAsset: symbol, out: 1000, } _, acc = testutils.NewAccount(ctx, am, 100) @@ -73,25 +71,25 @@ func TestFeeManager_calcTradeFeeForSingleTransfer(t *testing.T) { tran = Transfer{ inAsset: "BNB", in: 10000, - outAsset: "ABC-000", + outAsset: symbol, out: 100000, } fee = keeper.FeeManager.calcTradeFeeFromTransfer(acc.GetCoins(), &tran, keeper.engines) require.Equal(t, sdk.Coins{{"BNB", 5}}, fee.Tokens) tran = Transfer{ - inAsset: "ABC-000", + inAsset: symbol, in: 100000, outAsset: "XYZ-111", out: 100000, } - acc.SetCoins(sdk.Coins{{"ABC-000", 1000000}, {"BNB", 100}}) + acc.SetCoins(sdk.Coins{{symbol, 1000000}, {"BNB", 100}}) fee = keeper.FeeManager.calcTradeFeeFromTransfer(acc.GetCoins(), &tran, keeper.engines) require.Equal(t, sdk.Coins{{"BNB", 5}}, fee.Tokens) tran = Transfer{ inAsset: "XYZ-111", in: 100000, - outAsset: "ABC-000", + outAsset: symbol, out: 100000, } acc.SetCoins(sdk.Coins{{"XYZ-111", 1000000}, {"BNB", 1000}}) @@ -99,7 +97,23 @@ func TestFeeManager_calcTradeFeeForSingleTransfer(t *testing.T) { require.Equal(t, sdk.Coins{{"BNB", 500}}, fee.Tokens) } +func TestFeeManager_calcTradeFeeForSingleTransfer(t *testing.T) { + setChainVersion() + defer resetChainVersion() + symbol := "ABC-000" + feeManagerCalcTradeFeeForSingleTransfer(t, symbol) +} + +func TestFeeManager_calcTradeFeeForSingleTransferMini(t *testing.T) { + setChainVersion() + defer resetChainVersion() + symbol := "ABC-000M" + feeManagerCalcTradeFeeForSingleTransfer(t, symbol) +} + func TestFeeManager_CalcTradesFee(t *testing.T) { + setChainVersion() + defer resetChainVersion() ctx, am, keeper := setup() keeper.FeeManager.UpdateConfig(NewTestFeeConfig()) keeper.AddEngine(dextype.NewTradingPair("ABC-000", "BNB", 1e7)) @@ -108,6 +122,7 @@ func TestFeeManager_CalcTradesFee(t *testing.T) { keeper.AddEngine(dextype.NewTradingPair("XYZ-111", "BTC", 2e4)) keeper.AddEngine(dextype.NewTradingPair("BNB", "BTC", 5e5)) keeper.AddEngine(dextype.NewTradingPair("ABC-000", "XYZ-111", 6e7)) + keeper.AddEngine(dextype.NewTradingPair("ZYX-000M", "BNB", 1e8)) tradeTransfers := TradeTransfers{ {inAsset: "ABC-000", outAsset: "BNB", Oid: "1", in: 1e5, out: 2e4, Trade: &matcheng.Trade{}}, @@ -120,40 +135,48 @@ func TestFeeManager_CalcTradesFee(t *testing.T) { {inAsset: "BNB", outAsset: "ABC-000", Oid: "8", in: 5e8, out: 60e8, Trade: &matcheng.Trade{}}, {inAsset: "ABC-000", outAsset: "BNB", Oid: "9", in: 7e6, out: 5e5, Trade: &matcheng.Trade{}}, {inAsset: "ABC-000", outAsset: "BTC", Oid: "10", in: 6e5, out: 8e1, Trade: &matcheng.Trade{}}, + {inAsset: "ZYX-000M", outAsset: "BNB", Oid: "11", in: 2e7, out: 2e6, Trade: &matcheng.Trade{}}, } _, acc := testutils.NewAccount(ctx, am, 0) _ = acc.SetCoins(sdk.Coins{ {"ABC-000", 100e8}, {"BNB", 15251400}, {"BTC", 10e8}, - {"XYZ-000", 100e8}, + {"XYZ-111", 100e8}, + {"ZYX-000M", 100e8}, }) fees := keeper.FeeManager.CalcTradesFee(acc.GetCoins(), tradeTransfers, keeper.engines) - require.Equal(t, "ABC-000:8000;BNB:15251305;BTC:100000;XYZ-111:2000", fees.String()) + require.Equal(t, "ABC-000:8000;BNB:15251305;BTC:100000;XYZ-111:2000;ZYX-000M:20000", fees.String()) require.Equal(t, "BNB:250000", tradeTransfers[0].Fee.String()) require.Equal(t, "BNB:15000000", tradeTransfers[1].Fee.String()) require.Equal(t, "BNB:10", tradeTransfers[2].Fee.String()) require.Equal(t, "BNB:250", tradeTransfers[3].Fee.String()) require.Equal(t, "BTC:100000", tradeTransfers[4].Fee.String()) require.Equal(t, "BNB:1000", tradeTransfers[5].Fee.String()) - require.Equal(t, "BNB:15", tradeTransfers[6].Fee.String()) - require.Equal(t, "BNB:30", tradeTransfers[7].Fee.String()) - require.Equal(t, "ABC-000:8000", tradeTransfers[8].Fee.String()) - require.Equal(t, "XYZ-111:2000", tradeTransfers[9].Fee.String()) + require.Equal(t, "ZYX-000M:20000", tradeTransfers[6].Fee.String()) + require.Equal(t, "BNB:15", tradeTransfers[7].Fee.String()) + require.Equal(t, "BNB:30", tradeTransfers[8].Fee.String()) + require.Equal(t, "ABC-000:8000", tradeTransfers[9].Fee.String()) + require.Equal(t, "XYZ-111:2000", tradeTransfers[10].Fee.String()) + require.Equal(t, sdk.Coins{ {"ABC-000", 100e8}, {"BNB", 15251400}, {"BTC", 10e8}, - {"XYZ-000", 100e8}, + {"XYZ-111", 100e8}, + {"ZYX-000M", 100e8}, }, acc.GetCoins()) } func TestFeeManager_CalcExpiresFee(t *testing.T) { + setChainVersion() + defer resetChainVersion() ctx, am, keeper := setup() keeper.FeeManager.UpdateConfig(NewTestFeeConfig()) keeper.AddEngine(dextype.NewTradingPair("ABC-000", "BNB", 1e7)) keeper.AddEngine(dextype.NewTradingPair("XYZ-111", "BNB", 2e7)) keeper.AddEngine(dextype.NewTradingPair("BNB", "BTC", 5e5)) + keeper.AddEngine(dextype.NewTradingPair("ZYX-000M", "BNB", 1e8)) // in BNB expireTransfers := ExpireTransfers{ @@ -167,6 +190,7 @@ func TestFeeManager_CalcExpiresFee(t *testing.T) { {inAsset: "BNB", Symbol: "ABC-000_BNB", Oid: "8"}, {inAsset: "ABC-000", Symbol: "ABC-000_BNB", Oid: "9"}, {inAsset: "ABC-000", Symbol: "ABC-000_BTC", Oid: "10"}, + {inAsset: "ZYX-000M", Symbol: "ZYX-000M_BTC", Oid: "11"}, } _, acc := testutils.NewAccount(ctx, am, 0) _ = acc.SetCoins(sdk.Coins{ @@ -174,9 +198,10 @@ func TestFeeManager_CalcExpiresFee(t *testing.T) { {"BNB", 120000}, {"BTC", 10e8}, {"XYZ-111", 800000}, + {"ZYX-000M", 900000}, }) fees := keeper.FeeManager.CalcExpiresFee(acc.GetCoins(), eventFullyExpire, expireTransfers, keeper.engines, nil) - require.Equal(t, "ABC-000:1000000;BNB:120000;BTC:500;XYZ-111:800000", fees.String()) + require.Equal(t, "ABC-000:1000000;BNB:120000;BTC:500;XYZ-111:800000;ZYX-000M:100000", fees.String()) require.Equal(t, "BNB:20000", expireTransfers[0].Fee.String()) require.Equal(t, "BNB:20000", expireTransfers[1].Fee.String()) require.Equal(t, "BNB:20000", expireTransfers[2].Fee.String()) @@ -187,18 +212,34 @@ func TestFeeManager_CalcExpiresFee(t *testing.T) { require.Equal(t, "BTC:500", expireTransfers[7].Fee.String()) require.Equal(t, "XYZ-111:500000", expireTransfers[8].Fee.String()) require.Equal(t, "XYZ-111:300000", expireTransfers[9].Fee.String()) + require.Equal(t, "ZYX-000M:100000", expireTransfers[10].Fee.String()) require.Equal(t, sdk.Coins{ {"ABC-000", 100e8}, {"BNB", 120000}, {"BTC", 10e8}, {"XYZ-111", 800000}, + {"ZYX-000M", 900000}, }, acc.GetCoins()) } -func TestFeeManager_CalcTradeFee(t *testing.T) { +func TestFeeManager_calcTradeFee(t *testing.T) { + setChainVersion() + defer resetChainVersion() + symbol := "ABC-000" + feeManagerCalcTradeFee(t, symbol) +} + +func TestFeeManager_calcTradeFeeMini(t *testing.T) { + setChainVersion() + defer resetChainVersion() + symbol := "ABC-000M" + feeManagerCalcTradeFee(t, symbol) +} + +func feeManagerCalcTradeFee(t *testing.T, symbol string) { ctx, am, keeper := setup() keeper.FeeManager.UpdateConfig(NewTestFeeConfig()) - keeper.AddEngine(dextype.NewTradingPair("ABC-000", "BNB", 1e7)) + keeper.AddEngine(dextype.NewTradingPair(symbol, "BNB", 1e7)) // BNB _, acc := testutils.NewAccount(ctx, am, 0) // the tradeIn amount is large enough to make the fee > 0 @@ -213,38 +254,54 @@ func TestFeeManager_CalcTradeFee(t *testing.T) { // !BNB _, acc = testutils.NewAccount(ctx, am, 100) // has enough bnb - tradeIn = sdk.NewCoin("ABC-000", 1000e8) + tradeIn = sdk.NewCoin(symbol, 1000e8) acc.SetCoins(sdk.Coins{sdk.NewCoin(types.NativeTokenSymbol, 1e8)}) fee = keeper.FeeManager.CalcTradeFee(acc.GetCoins(), tradeIn, keeper.engines) require.Equal(t, sdk.Coins{sdk.NewCoin(types.NativeTokenSymbol, 5e6)}, fee.Tokens) // no enough bnb acc.SetCoins(sdk.Coins{sdk.NewCoin(types.NativeTokenSymbol, 1e6)}) fee = keeper.FeeManager.CalcTradeFee(acc.GetCoins(), tradeIn, keeper.engines) - require.Equal(t, sdk.Coins{sdk.NewCoin("ABC-000", 1e8)}, fee.Tokens) + require.Equal(t, sdk.Coins{sdk.NewCoin(symbol, 1e8)}, fee.Tokens) // very high price to produce int64 overflow - keeper.AddEngine(dextype.NewTradingPair("ABC-000", "BNB", 1e16)) + keeper.AddEngine(dextype.NewTradingPair(symbol, "BNB", 1e16)) // has enough bnb - tradeIn = sdk.NewCoin("ABC-000", 1000e8) + tradeIn = sdk.NewCoin(symbol, 1000e8) acc.SetCoins(sdk.Coins{sdk.NewCoin(types.NativeTokenSymbol, 1e16)}) fee = keeper.FeeManager.CalcTradeFee(acc.GetCoins(), tradeIn, keeper.engines) require.Equal(t, sdk.Coins{sdk.NewCoin(types.NativeTokenSymbol, 5e15)}, fee.Tokens) // no enough bnb, fee is within int64 acc.SetCoins(sdk.Coins{sdk.NewCoin(types.NativeTokenSymbol, 1e15)}) fee = keeper.FeeManager.CalcTradeFee(acc.GetCoins(), tradeIn, keeper.engines) - require.Equal(t, sdk.Coins{sdk.NewCoin("ABC-000", 1e8)}, fee.Tokens) + require.Equal(t, sdk.Coins{sdk.NewCoin(symbol, 1e8)}, fee.Tokens) // no enough bnb, even the fee overflows - tradeIn = sdk.NewCoin("ABC-000", 1e16) + tradeIn = sdk.NewCoin(symbol, 1e16) fee = keeper.FeeManager.CalcTradeFee(acc.GetCoins(), tradeIn, keeper.engines) - require.Equal(t, sdk.Coins{sdk.NewCoin("ABC-000", 1e13)}, fee.Tokens) + require.Equal(t, sdk.Coins{sdk.NewCoin(symbol, 1e13)}, fee.Tokens) } func TestFeeManager_CalcFixedFee(t *testing.T) { + setChainVersion() + defer resetChainVersion() + symbol1 := "ABC-000" + symbol2 := "BTC-000" + feeManagerCalcFixedFee(t, symbol1, symbol2) +} + +func TestFeeManager_CalcFixedFeeMini(t *testing.T) { + setChainVersion() + defer resetChainVersion() + symbol1 := "ABC-000M" + symbol2 := "BTC-000M" + feeManagerCalcFixedFee(t, symbol1, symbol2) +} + +func feeManagerCalcFixedFee(t *testing.T, symbol1 string, symbol2 string) { ctx, am, keeper := setup() keeper.FeeManager.UpdateConfig(NewTestFeeConfig()) _, acc := testutils.NewAccount(ctx, am, 1e4) - keeper.AddEngine(dextype.NewTradingPair("ABC-000", "BNB", 1e7)) - keeper.AddEngine(dextype.NewTradingPair("BNB", "BTC-000", 1e5)) + keeper.AddEngine(dextype.NewTradingPair(symbol1, "BNB", 1e7)) + keeper.AddEngine(dextype.NewTradingPair("BNB", symbol2, 1e5)) // in BNB // no enough BNB, but inAsset == BNB fee := keeper.FeeManager.CalcFixedFee(acc.GetCoins(), eventFullyExpire, types.NativeTokenSymbol, keeper.engines) @@ -261,36 +318,37 @@ func TestFeeManager_CalcFixedFee(t *testing.T) { require.Equal(t, sdk.Coins{sdk.NewCoin(types.NativeTokenSymbol, 2e4)}, fee.Tokens) // ABC-000_BNB, sell ABC-000 - fee = keeper.FeeManager.CalcFixedFee(acc.GetCoins(), eventFullyExpire, "ABC-000", keeper.engines) + fee = keeper.FeeManager.CalcFixedFee(acc.GetCoins(), eventFullyExpire, symbol1, keeper.engines) require.Equal(t, sdk.Coins{sdk.NewCoin(types.NativeTokenSymbol, 2e4)}, fee.Tokens) // No enough native token, but enough ABC-000 - acc.SetCoins(sdk.Coins{{Denom: types.NativeTokenSymbol, Amount: 1e4}, {Denom: "ABC-000", Amount: 1e8}}) - fee = keeper.FeeManager.CalcFixedFee(acc.GetCoins(), eventFullyExpire, "ABC-000", keeper.engines) - require.Equal(t, sdk.Coins{sdk.NewCoin("ABC-000", 1e6)}, fee.Tokens) + acc.SetCoins(sdk.Coins{{Denom: types.NativeTokenSymbol, Amount: 1e4}, {Denom: symbol1, Amount: 1e8}}) + fee = keeper.FeeManager.CalcFixedFee(acc.GetCoins(), eventFullyExpire, symbol1, keeper.engines) + require.Equal(t, sdk.Coins{sdk.NewCoin(symbol1, 1e6)}, fee.Tokens) // No enough native token and ABC-000 - acc.SetCoins(sdk.Coins{{Denom: types.NativeTokenSymbol, Amount: 1e4}, {Denom: "ABC-000", Amount: 1e5}}) - fee = keeper.FeeManager.CalcFixedFee(acc.GetCoins(), eventFullyExpire, "ABC-000", keeper.engines) - require.Equal(t, sdk.Coins{sdk.NewCoin("ABC-000", 1e5)}, fee.Tokens) + acc.SetCoins(sdk.Coins{{Denom: types.NativeTokenSymbol, Amount: 1e4}, {Denom: symbol1, Amount: 1e5}}) + fee = keeper.FeeManager.CalcFixedFee(acc.GetCoins(), eventFullyExpire, symbol1, keeper.engines) + require.Equal(t, sdk.Coins{sdk.NewCoin(symbol1, 1e5)}, fee.Tokens) // BNB_BTC-000, sell BTC-000 - acc.SetCoins(sdk.Coins{{Denom: "BTC-000", Amount: 1e4}}) - fee = keeper.FeeManager.CalcFixedFee(acc.GetCoins(), eventFullyExpire, "BTC-000", keeper.engines) - require.Equal(t, sdk.Coins{sdk.NewCoin("BTC-000", 1e2)}, fee.Tokens) + acc.SetCoins(sdk.Coins{{Denom: symbol2, Amount: 1e4}}) + fee = keeper.FeeManager.CalcFixedFee(acc.GetCoins(), eventFullyExpire, symbol2, keeper.engines) + require.Equal(t, sdk.Coins{sdk.NewCoin(symbol2, 1e2)}, fee.Tokens) // extreme prices - keeper.AddEngine(dextype.NewTradingPair("ABC-000", "BNB", 1)) - keeper.AddEngine(dextype.NewTradingPair("BNB", "BTC-000", 1e16)) - acc.SetCoins(sdk.Coins{{Denom: "ABC-000", Amount: 1e16}, {Denom: "BTC-000", Amount: 1e16}}) - fee = keeper.FeeManager.CalcFixedFee(acc.GetCoins(), eventFullyExpire, "ABC-000", keeper.engines) - require.Equal(t, sdk.Coins{sdk.NewCoin("ABC-000", 1e13)}, fee.Tokens) - fee = keeper.FeeManager.CalcFixedFee(acc.GetCoins(), eventFullyExpire, "BTC-000", keeper.engines) - require.Equal(t, sdk.Coins{sdk.NewCoin("BTC-000", 1e13)}, fee.Tokens) + keeper.AddEngine(dextype.NewTradingPair(symbol1, "BNB", 1)) + keeper.AddEngine(dextype.NewTradingPair("BNB", symbol2, 1e16)) + acc.SetCoins(sdk.Coins{{Denom: symbol1, Amount: 1e16}, {Denom: symbol2, Amount: 1e16}}) + fee = keeper.FeeManager.CalcFixedFee(acc.GetCoins(), eventFullyExpire, symbol1, keeper.engines) + require.Equal(t, sdk.Coins{sdk.NewCoin(symbol1, 1e13)}, fee.Tokens) + fee = keeper.FeeManager.CalcFixedFee(acc.GetCoins(), eventFullyExpire, symbol2, keeper.engines) + require.Equal(t, sdk.Coins{sdk.NewCoin(symbol2, 1e13)}, fee.Tokens) } func TestFeeManager_calcTradeFeeForSingleTransfer_SupportBUSD(t *testing.T) { - upgrade.Mgr.AddUpgradeHeight(upgrade.BEP70, -1) + setChainVersion() + defer resetChainVersion() ctx, am, keeper := setup() keeper.FeeManager.UpdateConfig(NewTestFeeConfig()) keeper.SetBUSDSymbol("BUSD-BD1") @@ -383,7 +441,8 @@ func TestFeeManager_calcTradeFeeForSingleTransfer_SupportBUSD(t *testing.T) { } func TestFeeManager_CalcFixedFee_SupportBUSD(t *testing.T) { - upgrade.Mgr.AddUpgradeHeight(upgrade.BEP70, -1) + setChainVersion() + defer resetChainVersion() ctx, am, keeper := setup() keeper.FeeManager.UpdateConfig(NewTestFeeConfig()) keeper.SetBUSDSymbol("BUSD-BD1") diff --git a/plugins/dex/order/handler.go b/plugins/dex/order/handler.go index 6a457f617..7903575a5 100644 --- a/plugins/dex/order/handler.go +++ b/plugins/dex/order/handler.go @@ -9,16 +9,13 @@ import ( "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth" "github.com/binance-chain/node/common/fees" common "github.com/binance-chain/node/common/types" "github.com/binance-chain/node/common/upgrade" me "github.com/binance-chain/node/plugins/dex/matcheng" - "github.com/binance-chain/node/plugins/dex/store" "github.com/binance-chain/node/plugins/dex/types" "github.com/binance-chain/node/plugins/dex/utils" - "github.com/binance-chain/node/wire" ) type NewOrderResponse struct { @@ -26,13 +23,13 @@ type NewOrderResponse struct { } // NewHandler - returns a handler for dex type messages. -func NewHandler(cdc *wire.Codec, k *Keeper, accKeeper auth.AccountKeeper) sdk.Handler { +func NewHandler(dexKeeper *DexKeeper) sdk.Handler { return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { switch msg := msg.(type) { case NewOrderMsg: - return handleNewOrder(ctx, cdc, k, msg, simulate) + return handleNewOrder(ctx, dexKeeper, msg) case CancelOrderMsg: - return handleCancelOrder(ctx, k, msg, simulate) + return handleCancelOrder(ctx, dexKeeper, msg) default: errMsg := fmt.Sprintf("Unrecognized dex msg type: %v", reflect.TypeOf(msg).Name()) return sdk.ErrUnknownRequest(errMsg).Result() @@ -40,51 +37,27 @@ func NewHandler(cdc *wire.Codec, k *Keeper, accKeeper auth.AccountKeeper) sdk.Ha } } -func validateOrder(ctx sdk.Context, pairMapper store.TradingPairMapper, acc sdk.Account, msg NewOrderMsg) error { - baseAsset, quoteAsset, err := utils.TradingPair2Assets(msg.Symbol) - if err != nil { - return err - } - - seq := acc.GetSequence() - expectedID := GenerateOrderID(seq, msg.Sender) - if expectedID != msg.Id { - return fmt.Errorf("the order ID given did not match the expected one: `%s`", expectedID) - } - - pair, err := pairMapper.GetTradingPair(ctx, baseAsset, quoteAsset) - if err != nil { - return err - } - - if msg.Quantity <= 0 || msg.Quantity%pair.LotSize.ToInt64() != 0 { - return fmt.Errorf("quantity(%v) is not rounded to lotSize(%v)", msg.Quantity, pair.LotSize.ToInt64()) - } - - if msg.Price <= 0 || msg.Price%pair.TickSize.ToInt64() != 0 { - return fmt.Errorf("price(%v) is not rounded to tickSize(%v)", msg.Price, pair.TickSize.ToInt64()) - } - - if sdk.IsUpgrade(upgrade.LotSizeOptimization) { - if utils.IsUnderMinNotional(msg.Price, msg.Quantity) { - return errors.New("notional value of the order is too small") - } - } - - if utils.IsExceedMaxNotional(msg.Price, msg.Quantity) { - return errors.New("notional value of the order is too large(cannot fit in int64)") - } - - return nil -} - -func validateQtyAndLockBalance(ctx sdk.Context, keeper *Keeper, acc common.NamedAccount, msg NewOrderMsg) error { +func validateQtyAndLockBalance(ctx sdk.Context, keeper *DexKeeper, acc common.NamedAccount, msg NewOrderMsg) error { symbol := strings.ToUpper(msg.Symbol) baseAssetSymbol, quoteAssetSymbol := utils.TradingPair2AssetsSafe(symbol) notional := utils.CalBigNotionalInt64(msg.Price, msg.Quantity) // note: the check sequence is well designed. freeBalance := acc.GetCoins() + + if sdk.IsUpgrade(sdk.BEP8) && isMiniSymbolPair(baseAssetSymbol, quoteAssetSymbol) { + var quantityBigEnough bool + if msg.Side == Side.BUY { + quantityBigEnough = msg.Quantity >= common.MiniTokenMinExecutionAmount + } else if msg.Side == Side.SELL { + quantityBigEnough = (msg.Quantity >= common.MiniTokenMinExecutionAmount) || freeBalance.AmountOf(symbol) == msg.Quantity + } + if !quantityBigEnough { + return fmt.Errorf("quantity is too small, the min quantity is %d or total free balance of the mini token", + common.MiniTokenMinExecutionAmount) + } + } + var toLockCoins sdk.Coins if msg.Side == Side.BUY { // for buy orders, @@ -126,16 +99,14 @@ func validateQtyAndLockBalance(ctx sdk.Context, keeper *Keeper, acc common.Named } func handleNewOrder( - ctx sdk.Context, cdc *wire.Codec, keeper *Keeper, msg NewOrderMsg, simulate bool, + ctx sdk.Context, dexKeeper *DexKeeper, msg NewOrderMsg, ) sdk.Result { - // TODO: the below is mostly copied from FreezeToken. It should be rewritten once "locked" becomes a field on account - // this check costs least. - if _, ok := keeper.OrderExists(msg.Symbol, msg.Id); ok { + if _, ok := dexKeeper.OrderExists(msg.Symbol, msg.Id); ok { errString := fmt.Sprintf("Duplicated order [%v] on symbol [%v]", msg.Id, msg.Symbol) return sdk.NewError(types.DefaultCodespace, types.CodeDuplicatedOrder, errString).Result() } - acc := keeper.am.GetAccount(ctx, msg.Sender).(common.NamedAccount) + acc := dexKeeper.am.GetAccount(ctx, msg.Sender).(common.NamedAccount) if !ctx.IsReCheckTx() { //for recheck: // 1. sequence is verified in anteHandler @@ -143,14 +114,15 @@ func handleNewOrder( // 3. trading pair is verified // 4. price/qty may have odd tick size/lot size, but it can be handled as // other existing orders. - err := validateOrder(ctx, keeper.PairMapper, acc, msg) + err := validateOrder(ctx, dexKeeper, acc, msg) + if err != nil { return sdk.NewError(types.DefaultCodespace, types.CodeInvalidOrderParam, err.Error()).Result() } } // the following is done in the app's checkstate / deliverstate, so it's safe to ignore isCheckTx - err := validateQtyAndLockBalance(ctx, keeper, acc, msg) + err := validateQtyAndLockBalance(ctx, dexKeeper, acc, msg) if err != nil { return sdk.NewError(types.DefaultCodespace, types.CodeInvalidOrderParam, err.Error()).Result() } @@ -168,7 +140,7 @@ func handleNewOrder( if txSrc, ok := ctx.Value(baseapp.TxSourceKey).(int64); ok { txSource = txSrc } else { - keeper.logger.Error("cannot get txSource from ctx") + dexKeeper.logger.Error("cannot get txSource from ctx") } }) msg := OrderInfo{ @@ -176,7 +148,9 @@ func handleNewOrder( height, timestamp, height, timestamp, 0, txHash, txSource} - err := keeper.AddOrder(msg, false) + + err := dexKeeper.AddOrder(msg, false) + if err != nil { return sdk.NewError(types.DefaultCodespace, types.CodeFailInsertOrder, err.Error()).Result() } @@ -200,9 +174,9 @@ func handleNewOrder( // Handle CancelOffer - func handleCancelOrder( - ctx sdk.Context, keeper *Keeper, msg CancelOrderMsg, simulate bool, + ctx sdk.Context, dexKeeper *DexKeeper, msg CancelOrderMsg, ) sdk.Result { - origOrd, ok := keeper.OrderExists(msg.Symbol, msg.RefId) + origOrd, ok := dexKeeper.OrderExists(msg.Symbol, msg.RefId) //only check whether there exists order to cancel if !ok { @@ -216,21 +190,21 @@ func handleCancelOrder( return sdk.NewError(types.DefaultCodespace, types.CodeFailLocateOrderToCancel, errString).Result() } - ord, err := keeper.GetOrder(origOrd.Id, origOrd.Symbol, origOrd.Side, origOrd.Price) + ord, err := dexKeeper.GetOrder(origOrd.Id, origOrd.Symbol, origOrd.Side, origOrd.Price) if err != nil { return sdk.NewError(types.DefaultCodespace, types.CodeFailLocateOrderToCancel, err.Error()).Result() } transfer := TransferFromCanceled(ord, origOrd, false) - sdkError := keeper.doTransfer(ctx, &transfer) + sdkError := dexKeeper.doTransfer(ctx, &transfer) if sdkError != nil { return sdkError.Result() } fee := common.Fee{} if !transfer.FeeFree() { - acc := keeper.am.GetAccount(ctx, msg.Sender) - fee = keeper.FeeManager.CalcFixedFee(acc.GetCoins(), transfer.eventType, transfer.inAsset, keeper.engines) + acc := dexKeeper.am.GetAccount(ctx, msg.Sender) + fee = dexKeeper.FeeManager.CalcFixedFee(acc.GetCoins(), transfer.eventType, transfer.inAsset, dexKeeper.GetEngines()) acc.SetCoins(acc.GetCoins().Minus(fee.Tokens)) - keeper.am.SetAccount(ctx, acc) + dexKeeper.am.SetAccount(ctx, acc) } // this is done in memory! we must not run this block in checktx or simulate! @@ -242,11 +216,11 @@ func handleCancelOrder( fees.Pool.AddFee(txHash, fee) } //remove order from cache and order book - err := keeper.RemoveOrder(origOrd.Id, origOrd.Symbol, func(ord me.OrderPart) { - if keeper.CollectOrderInfoForPublish { + err := dexKeeper.RemoveOrder(origOrd.Id, origOrd.Symbol, func(ord me.OrderPart) { + if dexKeeper.ShouldPublishOrder() { change := OrderChange{msg.RefId, Canceled, fee.String(), nil} - keeper.OrderChanges = append(keeper.OrderChanges, change) - keeper.updateRoundOrderFee(string(msg.Sender), fee) + dexKeeper.UpdateOrderChangeSync(change, msg.Symbol) + dexKeeper.updateRoundOrderFee(string(msg.Sender), fee) } }) if err != nil { @@ -256,3 +230,41 @@ func handleCancelOrder( return sdk.Result{} } + +func validateOrder(ctx sdk.Context, dexKeeper *DexKeeper, acc sdk.Account, msg NewOrderMsg) error { + baseAsset, quoteAsset, err := utils.TradingPair2Assets(msg.Symbol) + if err != nil { + return err + } + + seq := acc.GetSequence() + expectedID := GenerateOrderID(seq, msg.Sender) + if expectedID != msg.Id { + return fmt.Errorf("the order ID(%s) given did not match the expected one: `%s`", msg.Id, expectedID) + } + + pair, err := dexKeeper.PairMapper.GetTradingPair(ctx, baseAsset, quoteAsset) + if err != nil { + return err + } + + if msg.Quantity <= 0 || msg.Quantity%pair.LotSize.ToInt64() != 0 { + return fmt.Errorf("quantity(%v) is not rounded to lotSize(%v)", msg.Quantity, pair.LotSize.ToInt64()) + } + + if msg.Price <= 0 || msg.Price%pair.TickSize.ToInt64() != 0 { + return fmt.Errorf("price(%v) is not rounded to tickSize(%v)", msg.Price, pair.TickSize.ToInt64()) + } + + if sdk.IsUpgrade(upgrade.LotSizeOptimization) { + if utils.IsUnderMinNotional(msg.Price, msg.Quantity) { + return errors.New("notional value of the order is too small") + } + } + + if utils.IsExceedMaxNotional(msg.Price, msg.Quantity) { + return errors.New("notional value of the order is too large(cannot fit in int64)") + } + + return nil +} diff --git a/plugins/dex/order/handler_test.go b/plugins/dex/order/handler_test.go index a1903ff4d..4c0eace20 100644 --- a/plugins/dex/order/handler_test.go +++ b/plugins/dex/order/handler_test.go @@ -16,22 +16,24 @@ import ( "github.com/binance-chain/node/common" "github.com/binance-chain/node/plugins/dex/store" "github.com/binance-chain/node/plugins/dex/types" + dextypes "github.com/binance-chain/node/plugins/dex/types" "github.com/binance-chain/node/wire" ) -func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey, *sdk.KVStoreKey) { +func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey, *sdk.KVStoreKey, *sdk.KVStoreKey) { db := dbm.NewMemDB() key := sdk.NewKVStoreKey("pair") // TODO: can this be "pairs" as in the constant? key2 := sdk.NewKVStoreKey(common.AccountStoreName) + key3 := sdk.NewKVStoreKey(common.DexStoreName) ms := cstore.NewCommitMultiStore(db) ms.MountStoreWithDB(key, sdk.StoreTypeIAVL, db) ms.MountStoreWithDB(key2, sdk.StoreTypeIAVL, db) ms.LoadLatestVersion() - return ms, key, key2 + return ms, key, key2, key3 } -func setupMappers() (store.TradingPairMapper, auth.AccountKeeper, sdk.Context) { - ms, key, key2 := setupMultiStore() +func setupMappers() (store.TradingPairMapper, auth.AccountKeeper, sdk.Context, *DexKeeper) { + ms, key, key2, key3 := setupMultiStore() var cdc = wire.NewCodec() auth.RegisterBaseAccount(cdc) cdc.RegisterConcrete(types.TradingPair{}, "dex/TradingPair", nil) @@ -39,7 +41,8 @@ func setupMappers() (store.TradingPairMapper, auth.AccountKeeper, sdk.Context) { accMapper := auth.NewAccountKeeper(cdc, key2, auth.ProtoBaseAccount) accountCache := getAccountCache(cdc, ms, key2) ctx := sdk.NewContext(ms, abci.Header{}, sdk.RunTxModeDeliver, log.NewNopLogger()).WithAccountCache(accountCache) - return pairMapper, accMapper, ctx + keeper := NewDexKeeper(key3, accMapper, pairMapper, sdk.NewCodespacer().RegisterNext(dextypes.DefaultCodespace), 2, cdc, false) + return pairMapper, accMapper, ctx, keeper } func setupAccount(ctx sdk.Context, accMapper auth.AccountKeeper) (sdk.Account, sdk.AccAddress) { @@ -61,7 +64,7 @@ func setupAccount(ctx sdk.Context, accMapper auth.AccountKeeper) (sdk.Account, s } func TestHandler_ValidateOrder_OrderNotExist(t *testing.T) { - pairMapper, accMapper, ctx := setupMappers() + pairMapper, accMapper, ctx, keeper := setupMappers() pair := types.NewTradingPair("AAA-000", "BNB", 1e8) err := pairMapper.AddTradingPair(ctx, pair) require.NoError(t, err) @@ -76,14 +79,13 @@ func TestHandler_ValidateOrder_OrderNotExist(t *testing.T) { Id: fmt.Sprintf("%X-0", acc.GetAddress()), } - err = validateOrder(ctx, pairMapper, acc, msg) - + err = validateOrder(ctx, keeper, acc, msg) require.Error(t, err) require.Equal(t, fmt.Sprintf("trading pair not found: %s", msg.Symbol), err.Error()) } func TestHandler_ValidateOrder_WrongSymbol(t *testing.T) { - pairMapper, _, ctx := setupMappers() + _, _, ctx, keeper := setupMappers() msgs := []NewOrderMsg{ { @@ -104,14 +106,14 @@ func TestHandler_ValidateOrder_WrongSymbol(t *testing.T) { } for _, msg := range msgs { - err := validateOrder(ctx, pairMapper, nil, msg) + err := validateOrder(ctx, keeper, nil, msg) require.Error(t, err) require.Equal(t, fmt.Sprintf("Failed to parse trading pair symbol:%s into assets", msg.Symbol), err.Error()) } } func TestHandler_ValidateOrder_WrongPrice(t *testing.T) { - pairMapper, accMapper, ctx := setupMappers() + pairMapper, accMapper, ctx, keeper := setupMappers() pair := types.NewTradingPair("AAA-000", "BNB", 1e8) err := pairMapper.AddTradingPair(ctx, pair) require.NoError(t, err) @@ -126,13 +128,13 @@ func TestHandler_ValidateOrder_WrongPrice(t *testing.T) { Id: fmt.Sprintf("%X-0", acc.GetAddress()), } - err = validateOrder(ctx, pairMapper, acc, msg) + err = validateOrder(ctx, keeper, acc, msg) require.Error(t, err) require.Equal(t, fmt.Sprintf("price(%v) is not rounded to tickSize(%v)", msg.Price, pair.TickSize.ToInt64()), err.Error()) } func TestHandler_ValidateOrder_WrongQuantity(t *testing.T) { - pairMapper, accMapper, ctx := setupMappers() + pairMapper, accMapper, ctx, keeper := setupMappers() pair := types.NewTradingPair("AAA-000", "BNB", 1e8) err := pairMapper.AddTradingPair(ctx, pair) require.NoError(t, err) @@ -147,13 +149,13 @@ func TestHandler_ValidateOrder_WrongQuantity(t *testing.T) { Id: fmt.Sprintf("%X-0", acc.GetAddress()), } - err = validateOrder(ctx, pairMapper, acc, msg) + err = validateOrder(ctx, keeper, acc, msg) require.Error(t, err) require.Equal(t, fmt.Sprintf("quantity(%v) is not rounded to lotSize(%v)", msg.Quantity, pair.LotSize.ToInt64()), err.Error()) } func TestHandler_ValidateOrder_Normal(t *testing.T) { - pairMapper, accMapper, ctx := setupMappers() + pairMapper, accMapper, ctx, keeper := setupMappers() err := pairMapper.AddTradingPair(ctx, types.NewTradingPair("AAA-000", "BNB", 1e8)) require.NoError(t, err) @@ -167,12 +169,12 @@ func TestHandler_ValidateOrder_Normal(t *testing.T) { Id: fmt.Sprintf("%X-0", acc.GetAddress()), } - err = validateOrder(ctx, pairMapper, acc, msg) + err = validateOrder(ctx, keeper, acc, msg) require.NoError(t, err) } func TestHandler_ValidateOrder_MaxNotional(t *testing.T) { - pairMapper, accMapper, ctx := setupMappers() + pairMapper, accMapper, ctx, keeper := setupMappers() err := pairMapper.AddTradingPair(ctx, types.NewTradingPair("AAA-000", "BNB", 1e8)) require.NoError(t, err) @@ -186,7 +188,7 @@ func TestHandler_ValidateOrder_MaxNotional(t *testing.T) { Id: fmt.Sprintf("%X-0", acc.GetAddress()), } - err = validateOrder(ctx, pairMapper, acc, msg) + err = validateOrder(ctx, keeper, acc, msg) require.Error(t, err) require.Equal(t, "notional value of the order is too large(cannot fit in int64)", err.Error()) } diff --git a/plugins/dex/order/keeper.go b/plugins/dex/order/keeper.go index be384866f..b16a828d2 100644 --- a/plugins/dex/order/keeper.go +++ b/plugins/dex/order/keeper.go @@ -4,17 +4,18 @@ import ( "errors" "fmt" "math" + "math/big" "strings" "sync" "time" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + dbm "github.com/tendermint/tendermint/libs/db" tmlog "github.com/tendermint/tendermint/libs/log" tmstore "github.com/tendermint/tendermint/store" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth" - "github.com/binance-chain/node/common/fees" bnclog "github.com/binance-chain/node/common/log" "github.com/binance-chain/node/common/types" @@ -30,91 +31,127 @@ import ( ) const ( - numPricesStored = 2000 - pricesStoreEvery = 1000 - minimalNumPrices = 500 + BEP2TypeValue = 1 + MiniTypeValue = 2 preferencePriceLevel = 500 ) +type SymbolPairType int8 + +var PairType = struct { + BEP2 SymbolPairType + MINI SymbolPairType +}{BEP2TypeValue, MiniTypeValue} + var BUSDSymbol string type FeeHandler func(map[string]*types.Fee) type TransferHandler func(Transfer) -// in the future, this may be distributed via Sharding -type Keeper struct { +type DexKeeper struct { PairMapper store.TradingPairMapper - am auth.AccountKeeper storeKey sdk.StoreKey // The key used to access the store from the Context. codespace sdk.CodespaceType - engines map[string]*me.MatchEng - recentPrices map[string]*utils.FixedSizeRing // symbol -> latest "numPricesStored" prices per "pricesStoreEvery" blocks - allOrders map[string]map[string]*OrderInfo // symbol -> order ID -> order - OrderChangesMtx *sync.Mutex // guard OrderChanges and OrderInfosForPub during PreDevlierTx (which is async) - OrderChanges OrderChanges // order changed in this block, will be cleaned before matching for new block - OrderInfosForPub OrderInfoForPublish // for publication usage - roundOrders map[string][]string // limit to the total tx number in a block - roundIOCOrders map[string][]string - RoundOrderFees FeeHolder // order (and trade) related fee of this round, str of addr bytes -> fee - poolSize uint // number of concurrent channels, counted in the pow of 2 - cdc *wire.Codec + recentPrices map[string]*utils.FixedSizeRing // symbol -> latest "numPricesStored" prices per "pricesStoreEvery" blocks + am auth.AccountKeeper FeeManager *FeeManager - CollectOrderInfoForPublish bool + RoundOrderFees FeeHolder // order (and trade) related fee of this round, str of addr bytes -> fee + CollectOrderInfoForPublish bool //TODO separate for each order keeper + engines map[string]*me.MatchEng + pairsType map[string]SymbolPairType logger tmlog.Logger + poolSize uint // number of concurrent channels, counted in the pow of 2 + cdc *wire.Codec + OrderKeepers []DexOrderKeeper } -func CreateMatchEng(pairSymbol string, basePrice, lotSize int64) *me.MatchEng { - return me.NewMatchEng(pairSymbol, basePrice, lotSize, 0.05) -} - -// NewKeeper - Returns the Keeper -func NewKeeper(key sdk.StoreKey, am auth.AccountKeeper, tradingPairMapper store.TradingPairMapper, codespace sdk.CodespaceType, - concurrency uint, cdc *wire.Codec, collectOrderInfoForPublish bool) *Keeper { +func NewDexKeeper(key sdk.StoreKey, am auth.AccountKeeper, tradingPairMapper store.TradingPairMapper, codespace sdk.CodespaceType, concurrency uint, cdc *wire.Codec, collectOrderInfoForPublish bool) *DexKeeper { logger := bnclog.With("module", "dexkeeper") - return &Keeper{ + bep2OrderKeeper, miniOrderKeeper := NewBEP2OrderKeeper(), NewMiniOrderKeeper() + if collectOrderInfoForPublish { + bep2OrderKeeper.enablePublish() + miniOrderKeeper.enablePublish() + } + + return &DexKeeper{ PairMapper: tradingPairMapper, - am: am, storeKey: key, codespace: codespace, - engines: make(map[string]*me.MatchEng), recentPrices: make(map[string]*utils.FixedSizeRing, 256), - allOrders: make(map[string]map[string]*OrderInfo, 256), // need to init the nested map when a new symbol added. - OrderChangesMtx: &sync.Mutex{}, - OrderChanges: make(OrderChanges, 0), - OrderInfosForPub: make(OrderInfoForPublish), - roundOrders: make(map[string][]string, 256), - roundIOCOrders: make(map[string][]string, 256), + am: am, RoundOrderFees: make(map[string]*types.Fee, 256), + FeeManager: NewFeeManager(cdc, logger), + CollectOrderInfoForPublish: collectOrderInfoForPublish, + engines: make(map[string]*me.MatchEng), + pairsType: make(map[string]SymbolPairType), poolSize: concurrency, cdc: cdc, - FeeManager: NewFeeManager(cdc, key, logger), - CollectOrderInfoForPublish: collectOrderInfoForPublish, logger: logger, + OrderKeepers: []DexOrderKeeper{bep2OrderKeeper, miniOrderKeeper}, } } -func (kp *Keeper) SetBUSDSymbol(symbol string) { - BUSDSymbol = symbol -} - -func (kp *Keeper) Init(ctx sdk.Context, blockInterval, daysBack int, blockStore *tmstore.BlockStore, stateDB dbm.DB, lastHeight int64, txDecoder sdk.TxDecoder) { +func (kp *DexKeeper) Init(ctx sdk.Context, blockInterval, daysBack int, blockStore *tmstore.BlockStore, stateDB dbm.DB, lastHeight int64, txDecoder sdk.TxDecoder) { kp.initOrderBook(ctx, blockInterval, daysBack, blockStore, stateDB, lastHeight, txDecoder) kp.InitRecentPrices(ctx) } -func (kp *Keeper) InitRecentPrices(ctx sdk.Context) { +func (kp *DexKeeper) InitRecentPrices(ctx sdk.Context) { kp.recentPrices = kp.PairMapper.GetRecentPrices(ctx, pricesStoreEvery, numPricesStored) } -func (kp *Keeper) AddEngine(pair dexTypes.TradingPair) *me.MatchEng { - symbol := strings.ToUpper(pair.GetSymbol()) - eng := CreateMatchEng(symbol, pair.ListPrice.ToInt64(), pair.LotSize.ToInt64()) - kp.engines[symbol] = eng - kp.allOrders[symbol] = map[string]*OrderInfo{} - return eng +func (kp *DexKeeper) SetBUSDSymbol(symbol string) { + BUSDSymbol = symbol +} + +func (kp *DexKeeper) EnablePublish() { + kp.CollectOrderInfoForPublish = true + for i := range kp.OrderKeepers { + kp.OrderKeepers[i].enablePublish() + } +} + +func (kp *DexKeeper) GetPairType(symbol string) SymbolPairType { + pairType, ok := kp.pairsType[symbol] + if !ok { + if dexUtils.IsMiniTokenTradingPair(symbol) { + pairType = PairType.MINI + } else { + pairType = PairType.BEP2 + } + } + return pairType +} + +func (kp *DexKeeper) getOrderKeeper(symbol string) (DexOrderKeeper, error) { + pairType, ok := kp.pairsType[symbol] + if !ok { + err := fmt.Errorf("invalid symbol: %s", symbol) + kp.logger.Debug(err.Error()) + return nil, err + } + for i := range kp.OrderKeepers { + if kp.OrderKeepers[i].supportPairType(pairType) { + return kp.OrderKeepers[i], nil + } + } + err := fmt.Errorf("failed to find orderKeeper for symbol pair [%s]", symbol) + kp.logger.Error(err.Error()) + return nil, err +} + +func (kp *DexKeeper) mustGetOrderKeeper(symbol string) DexOrderKeeper { + pairType := kp.pairsType[symbol] + for i := range kp.OrderKeepers { + if kp.OrderKeepers[i].supportPairType(pairType) { + return kp.OrderKeepers[i] + } + } + + panic(fmt.Errorf("invalid symbol %s", symbol)) } -func (kp *Keeper) UpdateTickSizeAndLotSize(ctx sdk.Context) { +func (kp *DexKeeper) UpdateTickSizeAndLotSize(ctx sdk.Context) { tradingPairs := kp.PairMapper.ListAllTradingPairs(ctx) lotSizeCache := make(map[string]int64) // baseAsset -> lotSize for _, pair := range tradingPairs { @@ -138,7 +175,7 @@ func (kp *Keeper) UpdateTickSizeAndLotSize(ctx sdk.Context) { } } -func (kp *Keeper) determineTickAndLotSize(pair dexTypes.TradingPair, priceWMA int64, lotSizeCache map[string]int64) (tickSize, lotSize int64) { +func (kp *DexKeeper) determineTickAndLotSize(pair dexTypes.TradingPair, priceWMA int64, lotSizeCache map[string]int64) (tickSize, lotSize int64) { tickSize = dexUtils.CalcTickSize(priceWMA) if !sdk.IsUpgrade(upgrade.LotSizeOptimization) { lotSize = dexUtils.CalcLotSize(priceWMA) @@ -153,7 +190,7 @@ func (kp *Keeper) determineTickAndLotSize(pair dexTypes.TradingPair, priceWMA in return } -func (kp *Keeper) DetermineLotSize(baseAssetSymbol, quoteAssetSymbol string, price int64) (lotSize int64) { +func (kp *DexKeeper) DetermineLotSize(baseAssetSymbol, quoteAssetSymbol string, price int64) (lotSize int64) { var priceAgainstNative int64 if baseAssetSymbol == types.NativeTokenSymbol { // price of BNB/BNB is 1e8 @@ -161,17 +198,23 @@ func (kp *Keeper) DetermineLotSize(baseAssetSymbol, quoteAssetSymbol string, pri } else if quoteAssetSymbol == types.NativeTokenSymbol { priceAgainstNative = price } else { - if ps, ok := kp.recentPrices[dexUtils.Assets2TradingPair(baseAssetSymbol, types.NativeTokenSymbol)]; ok { - priceAgainstNative = dexUtils.CalcPriceWMA(ps) - } else if ps, ok = kp.recentPrices[dexUtils.Assets2TradingPair(types.NativeTokenSymbol, baseAssetSymbol)]; ok { - wma := dexUtils.CalcPriceWMA(ps) - priceAgainstNative = 1e16 / wma - } else { - // the recentPrices still have not collected any price yet, iff the native pair is listed for less than kp.pricesStoreEvery blocks - if engine, ok := kp.engines[dexUtils.Assets2TradingPair(baseAssetSymbol, types.NativeTokenSymbol)]; ok { - priceAgainstNative = engine.LastTradePrice - } else if engine, ok = kp.engines[dexUtils.Assets2TradingPair(types.NativeTokenSymbol, baseAssetSymbol)]; ok { - priceAgainstNative = 1e16 / engine.LastTradePrice + var found bool + priceAgainstNative, found = kp.calcPriceAgainst(baseAssetSymbol, types.NativeTokenSymbol) + if !found { + if sdk.IsUpgrade(upgrade.BEP70) && len(BUSDSymbol) > 0 { + var tmp = big.NewInt(0) + priceAgainstBUSD, ok := kp.calcPriceAgainst(baseAssetSymbol, BUSDSymbol) + if !ok { + // for newly added pair, there is no trading yet + priceAgainstBUSD = price + } + priceBUSDAgainstNative, _ := kp.calcPriceAgainst(BUSDSymbol, types.NativeTokenSymbol) + tmp = tmp.Div(tmp.Mul(big.NewInt(priceAgainstBUSD), big.NewInt(priceBUSDAgainstNative)), big.NewInt(1e8)) + if tmp.IsInt64() { + priceAgainstNative = tmp.Int64() + } else { + priceAgainstNative = math.MaxInt64 + } } else { // should not happen kp.logger.Error("DetermineLotSize failed because no native pair found", "base", baseAssetSymbol, "quote", quoteAssetSymbol) @@ -182,7 +225,31 @@ func (kp *Keeper) DetermineLotSize(baseAssetSymbol, quoteAssetSymbol string, pri return lotSize } -func (kp *Keeper) UpdateLotSize(symbol string, lotSize int64) { +func (kp *DexKeeper) calcPriceAgainst(symbol, targetSymbol string) (int64, bool) { + var priceAgainst int64 = 0 + var found bool + if ps, ok := kp.recentPrices[dexUtils.Assets2TradingPair(symbol, targetSymbol)]; ok { + priceAgainst = dexUtils.CalcPriceWMA(ps) + found = true + } else if ps, ok = kp.recentPrices[dexUtils.Assets2TradingPair(targetSymbol, symbol)]; ok { + wma := dexUtils.CalcPriceWMA(ps) + priceAgainst = 1e16 / wma + found = true + } else { + // the recentPrices still have not collected any price yet, iff the native pair is listed for less than kp.pricesStoreEvery blocks + if engine, ok := kp.engines[dexUtils.Assets2TradingPair(symbol, targetSymbol)]; ok { + priceAgainst = engine.LastTradePrice + found = true + } else if engine, ok = kp.engines[dexUtils.Assets2TradingPair(targetSymbol, symbol)]; ok { + priceAgainst = 1e16 / engine.LastTradePrice + found = true + } + } + + return priceAgainst, found +} + +func (kp *DexKeeper) UpdateLotSize(symbol string, lotSize int64) { eng, ok := kp.engines[symbol] if !ok { panic(fmt.Sprintf("match engine of symbol %s doesn't exist", symbol)) @@ -190,7 +257,25 @@ func (kp *Keeper) UpdateLotSize(symbol string, lotSize int64) { eng.LotSize = lotSize } -func (kp *Keeper) AddOrder(info OrderInfo, isRecovery bool) (err error) { +func (kp *DexKeeper) AddEngine(pair dexTypes.TradingPair) *me.MatchEng { + symbol := strings.ToUpper(pair.GetSymbol()) + eng := CreateMatchEng(symbol, pair.ListPrice.ToInt64(), pair.LotSize.ToInt64()) + kp.engines[symbol] = eng + pairType := PairType.BEP2 + if dexUtils.IsMiniTokenTradingPair(symbol) { + pairType = PairType.MINI + } + kp.pairsType[symbol] = pairType + for i := range kp.OrderKeepers { + if kp.OrderKeepers[i].supportPairType(pairType) { + kp.OrderKeepers[i].initOrders(symbol) + break + } + } + return eng +} + +func (kp *DexKeeper) AddOrder(info OrderInfo, isRecovery bool) (err error) { //try update order book first symbol := strings.ToUpper(info.Symbol) eng, ok := kp.engines[symbol] @@ -204,26 +289,7 @@ func (kp *Keeper) AddOrder(info OrderInfo, isRecovery bool) (err error) { return err } - if kp.CollectOrderInfoForPublish { - change := OrderChange{info.Id, Ack, "", nil} - // deliberately not add this message to orderChanges - if !isRecovery { - kp.OrderChanges = append(kp.OrderChanges, change) - } - kp.logger.Debug("add order to order changes map", "orderId", info.Id, "isRecovery", isRecovery) - kp.OrderInfosForPub[info.Id] = &info - } - - kp.allOrders[symbol][info.Id] = &info - if ids, ok := kp.roundOrders[symbol]; ok { - kp.roundOrders[symbol] = append(ids, info.Id) - } else { - newIds := make([]string, 0, 16) - kp.roundOrders[symbol] = append(newIds, info.Id) - } - if info.TimeInForce == TimeInForce.IOC { - kp.roundIOCOrders[symbol] = append(kp.roundIOCOrders[symbol], info.Id) - } + kp.mustGetOrderKeeper(symbol).addOrder(symbol, info, isRecovery) kp.logger.Debug("Added orders", "symbol", symbol, "id", info.Id) return nil } @@ -232,29 +298,22 @@ func orderNotFound(symbol, id string) error { return fmt.Errorf("Failed to find order [%v] on symbol [%v]", id, symbol) } -func (kp *Keeper) RemoveOrder(id string, symbol string, postCancelHandler func(ord me.OrderPart)) (err error) { +func (kp *DexKeeper) RemoveOrder(id string, symbol string, postCancelHandler func(ord me.OrderPart)) error { symbol = strings.ToUpper(symbol) - ordMsg, ok := kp.OrderExists(symbol, id) - if !ok { - return orderNotFound(symbol, id) - } - eng, ok := kp.engines[symbol] - if !ok { - return orderNotFound(symbol, id) - } - delete(kp.allOrders[symbol], id) - ord, err := eng.Book.RemoveOrder(id, ordMsg.Side, ordMsg.Price) - if err != nil { - return err - } - - if postCancelHandler != nil { - postCancelHandler(ord) + if dexOrderKeeper, err := kp.getOrderKeeper(symbol); err == nil { + ord, err := dexOrderKeeper.removeOrder(kp, id, symbol) + if err != nil { + return err + } + if postCancelHandler != nil { + postCancelHandler(ord) + } + return nil } - return nil + return orderNotFound(symbol, id) } -func (kp *Keeper) GetOrder(id string, symbol string, side int8, price int64) (ord me.OrderPart, err error) { +func (kp *DexKeeper) GetOrder(id string, symbol string, side int8, price int64) (ord me.OrderPart, err error) { symbol = strings.ToUpper(symbol) _, ok := kp.OrderExists(symbol, id) if !ok { @@ -267,11 +326,9 @@ func (kp *Keeper) GetOrder(id string, symbol string, side int8, price int64) (or return eng.Book.GetOrder(id, side, price) } -func (kp *Keeper) OrderExists(symbol, id string) (OrderInfo, bool) { - if orders, ok := kp.allOrders[symbol]; ok { - if msg, ok := orders[id]; ok { - return *msg, ok - } +func (kp *DexKeeper) OrderExists(symbol, id string) (OrderInfo, bool) { + if dexOrderKeeper, err := kp.getOrderKeeper(symbol); err == nil { + return dexOrderKeeper.orderExists(symbol, id) } return OrderInfo{}, false } @@ -334,81 +391,7 @@ func channelHash(accAddress sdk.AccAddress, bucketNumber int) int { return sum % bucketNumber } -func (kp *Keeper) matchAndDistributeTradesForSymbol(symbol string, height, timestamp int64, orders map[string]*OrderInfo, - distributeTrade bool, tradeOuts []chan Transfer) { - engine := kp.engines[symbol] - concurrency := len(tradeOuts) - // please note there is no logging in matching, expecting to see the order book details - // from the exchange's order book stream. - if engine.Match(height) { - kp.logger.Debug("Match finish:", "symbol", symbol, "lastTradePrice", engine.LastTradePrice) - for i := range engine.Trades { - t := &engine.Trades[i] - updateOrderMsg(orders[t.Bid], t.BuyCumQty, height, timestamp) - updateOrderMsg(orders[t.Sid], t.SellCumQty, height, timestamp) - if distributeTrade { - t1, t2 := TransferFromTrade(t, symbol, kp.allOrders[symbol]) - c := channelHash(t1.accAddress, concurrency) - tradeOuts[c] <- t1 - c = channelHash(t2.accAddress, concurrency) - tradeOuts[c] <- t2 - } - } - droppedIds := engine.DropFilledOrder() //delete from order books - for _, id := range droppedIds { - delete(orders, id) //delete from order cache - } - kp.logger.Debug("Drop filled orders", "total", droppedIds) - } else { - // FUTURE-TODO: - // when Match() failed, have to unsolicited cancel all the new orders - // in this block. Ideally the order IDs would be stored in the EndBlock response, - // but this is not implemented yet, pending Tendermint to better handle EndBlock - // for index service. - kp.logger.Error("Fatal error occurred in matching, cancel all incoming new orders", - "symbol", symbol) - thisRoundIds := kp.roundOrders[symbol] - for _, id := range thisRoundIds { - msg := orders[id] - delete(orders, id) - if ord, err := engine.Book.RemoveOrder(id, msg.Side, msg.Price); err == nil { - kp.logger.Info("Removed due to match failure", "ordID", msg.Id) - if distributeTrade { - c := channelHash(msg.Sender, concurrency) - tradeOuts[c] <- TransferFromCanceled(ord, *msg, true) - } - } else { - kp.logger.Error("Failed to remove order, may be fatal!", "orderID", id) - } - - // let the order status publisher publish these abnormal - // order status change outs. - if kp.CollectOrderInfoForPublish { - kp.OrderChangesMtx.Lock() - kp.OrderChanges = append(kp.OrderChanges, OrderChange{id, FailedMatching, "", nil}) - kp.OrderChangesMtx.Unlock() - } - } - return // no need to handle IOC - } - iocIDs := kp.roundIOCOrders[symbol] - for _, id := range iocIDs { - if msg, ok := orders[id]; ok { - delete(orders, id) - if ord, err := engine.Book.RemoveOrder(id, msg.Side, msg.Price); err == nil { - kp.logger.Debug("Removed unclosed IOC order", "ordID", msg.Id) - if distributeTrade { - c := channelHash(msg.Sender, concurrency) - tradeOuts[c] <- TransferFromExpired(ord, *msg) - } - } else { - kp.logger.Error("Failed to remove IOC order, may be fatal!", "orderID", id) - } - } - } -} - -func (kp *Keeper) SubscribeParamChange(hub *paramhub.Keeper) { +func (kp *DexKeeper) SubscribeParamChange(hub *paramhub.Keeper) { hub.SubscribeParamChange( func(ctx sdk.Context, changes []interface{}) { for _, c := range changes { @@ -446,109 +429,35 @@ func (kp *Keeper) SubscribeParamChange(hub *paramhub.Keeper) { }) } -// Run as postConsume procedure of async, no concurrent updates of orders map -func updateOrderMsg(order *OrderInfo, cumQty, height, timestamp int64) { - order.CumQty = cumQty - order.LastUpdatedHeight = height - order.LastUpdatedTimestamp = timestamp -} - -// please note if distributeTrade this method will work in async mode, otherwise in sync mode. -func (kp *Keeper) matchAndDistributeTrades(distributeTrade bool, height, timestamp int64) []chan Transfer { - size := len(kp.roundOrders) - // size is the number of pairs that have new orders, i.e. it should call match() - if size == 0 { - kp.logger.Info("No new orders for any pair, give up matching") - return nil - } - - concurrency := 1 << kp.poolSize - tradeOuts := make([]chan Transfer, concurrency) - if distributeTrade { - ordNum := 0 - for _, perSymbol := range kp.roundOrders { - ordNum += len(perSymbol) - } - for i := range tradeOuts { - //assume every new order would have 2 trades and generate 4 transfer - tradeOuts[i] = make(chan Transfer, ordNum*4/concurrency) - } - } - - symbolCh := make(chan string, concurrency) - producer := func() { - for symbol := range kp.roundOrders { - symbolCh <- symbol - } - close(symbolCh) - } - matchWorker := func() { - i := 0 - for symbol := range symbolCh { - i++ - kp.matchAndDistributeTradesForSymbol(symbol, height, timestamp, kp.allOrders[symbol], distributeTrade, tradeOuts) - } - } - - if distributeTrade { - utils.ConcurrentExecuteAsync(concurrency, producer, matchWorker, func() { - for _, tradeOut := range tradeOuts { - close(tradeOut) - } - }) - } else { - utils.ConcurrentExecuteSync(concurrency, producer, matchWorker) - } - return tradeOuts -} - -func (kp *Keeper) GetOrderBookLevels(pair string, maxLevels int) (orderbook []store.OrderBookLevel, pendingMatch bool) { +func (kp *DexKeeper) GetOrderBookLevels(pair string, maxLevels int) (orderbook []store.OrderBookLevel, pendingMatch bool) { orderbook = make([]store.OrderBookLevel, maxLevels) i, j := 0, 0 - if eng, ok := kp.engines[pair]; ok { // TODO: check considered bucket splitting? eng.Book.ShowDepth(maxLevels, func(p *me.PriceLevel, levelIndex int) { orderbook[i].BuyPrice = utils.Fixed8(p.Price) orderbook[i].BuyQty = utils.Fixed8(p.TotalLeavesQty()) i++ - }, - func(p *me.PriceLevel, levelIndex int) { - orderbook[j].SellPrice = utils.Fixed8(p.Price) - orderbook[j].SellQty = utils.Fixed8(p.TotalLeavesQty()) - j++ - }) - pendingMatch = len(kp.roundOrders[pair]) > 0 + }, func(p *me.PriceLevel, levelIndex int) { + orderbook[j].SellPrice = utils.Fixed8(p.Price) + orderbook[j].SellQty = utils.Fixed8(p.TotalLeavesQty()) + j++ + }) + roundOrders := kp.mustGetOrderKeeper(pair).getRoundOrdersForPair(pair) + pendingMatch = len(roundOrders) > 0 } return orderbook, pendingMatch } -func (kp *Keeper) GetOpenOrders(pair string, addr sdk.AccAddress) []store.OpenOrder { - openOrders := make([]store.OpenOrder, 0) - - for _, order := range kp.allOrders[pair] { - if string(order.Sender.Bytes()) == string(addr.Bytes()) { - openOrders = append( - openOrders, - store.OpenOrder{ - order.Id, - pair, - utils.Fixed8(order.Price), - utils.Fixed8(order.Quantity), - utils.Fixed8(order.CumQty), - order.CreatedHeight, - order.CreatedTimestamp, - order.LastUpdatedHeight, - order.LastUpdatedTimestamp, - }) - } +func (kp *DexKeeper) GetOpenOrders(pair string, addr sdk.AccAddress) []store.OpenOrder { + if dexOrderKeeper, err := kp.getOrderKeeper(pair); err == nil { + return dexOrderKeeper.getOpenOrders(pair, addr) } - - return openOrders + return make([]store.OpenOrder, 0) } -func (kp *Keeper) GetOrderBooks(maxLevels int) ChangedPriceLevelsMap { +func (kp *DexKeeper) GetOrderBooks(maxLevels int) ChangedPriceLevelsMap { var res = make(ChangedPriceLevelsMap) for pair, eng := range kp.engines { buys := make(map[int64]int64) @@ -566,7 +475,7 @@ func (kp *Keeper) GetOrderBooks(maxLevels int) ChangedPriceLevelsMap { return res } -func (kp *Keeper) GetPriceLevel(pair string, side int8, price int64) *me.PriceLevel { +func (kp *DexKeeper) GetPriceLevel(pair string, side int8, price int64) *me.PriceLevel { if eng, ok := kp.engines[pair]; ok { return eng.Book.GetPriceLevel(price, side) } else { @@ -574,7 +483,7 @@ func (kp *Keeper) GetPriceLevel(pair string, side int8, price int64) *me.PriceLe } } -func (kp *Keeper) GetLastTrades(height int64, pair string) ([]me.Trade, int64) { +func (kp *DexKeeper) GetLastTrades(height int64, pair string) ([]me.Trade, int64) { if eng, ok := kp.engines[pair]; ok { if eng.LastMatchHeight == height { return eng.Trades, eng.LastTradePrice @@ -584,24 +493,28 @@ func (kp *Keeper) GetLastTrades(height int64, pair string) ([]me.Trade, int64) { } // !!! FOR TEST USE ONLY -func (kp *Keeper) GetLastTradesForPair(pair string) ([]me.Trade, int64) { +func (kp *DexKeeper) GetLastTradesForPair(pair string) ([]me.Trade, int64) { if eng, ok := kp.engines[pair]; ok { return eng.Trades, eng.LastTradePrice } return nil, 0 } -func (kp *Keeper) ClearOrderBook(pair string) { +func (kp *DexKeeper) ClearOrderBook(pair string) { if eng, ok := kp.engines[pair]; ok { eng.Book.Clear() } } -func (kp *Keeper) ClearOrderChanges() { - kp.OrderChanges = kp.OrderChanges[:0] +func (kp *DexKeeper) ClearOrderChanges() { + for _, orderKeeper := range kp.OrderKeepers { + if orderKeeper.supportUpgradeVersion() { + orderKeeper.clearOrderChanges() + } + } } -func (kp *Keeper) doTransfer(ctx sdk.Context, tran *Transfer) sdk.Error { +func (kp *DexKeeper) doTransfer(ctx sdk.Context, tran *Transfer) sdk.Error { account := kp.am.GetAccount(ctx, tran.accAddress).(types.NamedAccount) newLocked := account.GetLockedCoins().Minus(sdk.Coins{sdk.NewCoin(tran.outAsset, tran.unlock)}) // these two non-negative check are to ensure the Transfer gen result is correct before we actually operate the acc. @@ -628,12 +541,15 @@ func (kp *Keeper) doTransfer(ctx sdk.Context, tran *Transfer) sdk.Error { return nil } -func (kp *Keeper) clearAfterMatch() { - kp.roundOrders = make(map[string][]string, 256) - kp.roundIOCOrders = make(map[string][]string, 256) +func (kp *DexKeeper) ClearAfterMatch() { + for _, orderKeeper := range kp.OrderKeepers { + if orderKeeper.supportUpgradeVersion() { + orderKeeper.clearAfterMatch() + } + } } -func (kp *Keeper) StoreTradePrices(ctx sdk.Context) { +func (kp *DexKeeper) StoreTradePrices(ctx sdk.Context) { // TODO: check block height != 0 if ctx.BlockHeight()%pricesStoreEvery == 0 { lastTradePrices := make(map[string]int64, len(kp.engines)) @@ -650,7 +566,7 @@ func (kp *Keeper) StoreTradePrices(ctx sdk.Context) { } } -func (kp *Keeper) allocate(ctx sdk.Context, tranCh <-chan Transfer, postAllocateHandler func(tran Transfer)) ( +func (kp *DexKeeper) allocate(ctx sdk.Context, tranCh <-chan Transfer, postAllocateHandler func(tran Transfer)) ( types.Fee, map[string]*types.Fee) { if !sdk.IsUpgrade(upgrade.BEP19) { return kp.allocateBeforeGalileo(ctx, tranCh, postAllocateHandler) @@ -724,7 +640,7 @@ func (kp *Keeper) allocate(ctx sdk.Context, tranCh <-chan Transfer, postAllocate } // DEPRECATED -func (kp *Keeper) allocateBeforeGalileo(ctx sdk.Context, tranCh <-chan Transfer, postAllocateHandler func(tran Transfer)) ( +func (kp *DexKeeper) allocateBeforeGalileo(ctx sdk.Context, tranCh <-chan Transfer, postAllocateHandler func(tran Transfer)) ( types.Fee, map[string]*types.Fee) { // use string of the addr as the key since map makes a fast path for string key. // Also, making the key have same length is also an optimization. @@ -805,11 +721,10 @@ func (kp *Keeper) allocateBeforeGalileo(ctx sdk.Context, tranCh <-chan Transfer, return totalFee, feesPerAcc } -func (kp *Keeper) allocateAndCalcFee( +func (kp *DexKeeper) allocateAndCalcFee( ctx sdk.Context, tradeOuts []chan Transfer, - postAlloTransHandler TransferHandler, -) types.Fee { + postAlloTransHandler TransferHandler) types.Fee { concurrency := len(tradeOuts) var wg sync.WaitGroup wg.Add(concurrency) @@ -840,36 +755,14 @@ func (kp *Keeper) allocateAndCalcFee( return totalFee } -// MatchAll will only concurrently match but do not allocate into accounts -func (kp *Keeper) MatchAll() { - tradeOuts := kp.matchAndDistributeTrades(false) //only match - if tradeOuts == nil { - kp.logger.Info("No order comes in for the block") - } - kp.clearAfterMatch() -} - -// MatchAndAllocateAll() is concurrently matching and allocating across -// all the symbols' order books, among all the clients -// Return whether match has been done in this height -func (kp *Keeper) MatchAndAllocateAll( - ctx sdk.Context, - postAlloTransHandler TransferHandler, -) { - kp.logger.Debug("Start Matching for all...", "height", ctx.BlockHeader().Height, "symbolNum", len(kp.roundOrders)) - timestamp := ctx.BlockHeader().Time.UnixNano() - tradeOuts := kp.matchAndDistributeTrades(true, ctx.BlockHeader().Height, timestamp) - if tradeOuts == nil { - kp.logger.Info("No order comes in for the block") +func (kp *DexKeeper) expireOrders(ctx sdk.Context, blockTime time.Time) []chan Transfer { + allOrders := make(map[string]map[string]*OrderInfo) //TODO replace by iterator + for _, orderKeeper := range kp.OrderKeepers { + if orderKeeper.supportUpgradeVersion() { + allOrders = appendAllOrdersMap(allOrders, orderKeeper.getAllOrders()) + } } - - totalFee := kp.allocateAndCalcFee(ctx, tradeOuts, postAlloTransHandler) - fees.Pool.AddAndCommitFee("MATCH", totalFee) - kp.clearAfterMatch() -} - -func (kp *Keeper) expireOrders(ctx sdk.Context, blockTime int64) []chan Transfer { - size := len(kp.allOrders) + size := len(allOrders) if size == 0 { kp.logger.Info("No orders to expire") return nil @@ -914,14 +807,14 @@ func (kp *Keeper) expireOrders(ctx sdk.Context, blockTime int64) []chan Transfer symbolCh := make(chan string, concurrency) utils.ConcurrentExecuteAsync(concurrency, func() { - for symbol := range kp.allOrders { + for symbol := range allOrders { symbolCh <- symbol } close(symbolCh) }, func() { for symbol := range symbolCh { engine := kp.engines[symbol] - orders := kp.allOrders[symbol] + orders := allOrders[symbol] expire(orders, engine, me.BUYSIDE) expire(orders, engine, me.SELLSIDE) } @@ -934,7 +827,7 @@ func (kp *Keeper) expireOrders(ctx sdk.Context, blockTime int64) []chan Transfer return transferChs } -func (kp *Keeper) getExpireHeight(ctx sdk.Context, blockTime time.Time) (expireHeight, forceExpireHeight int64, noBreatheBlock error) { +func (kp *DexKeeper) getExpireHeight(ctx sdk.Context, blockTime time.Time) (expireHeight, forceExpireHeight int64, noBreatheBlock error) { const effectiveDays = 3 expireHeight, noBreatheBlock = kp.GetBreatheBlockHeight(ctx, blockTime, effectiveDays) if noBreatheBlock != nil { @@ -958,7 +851,7 @@ func (kp *Keeper) getExpireHeight(ctx sdk.Context, blockTime time.Time) (expireH return expireHeight, forceExpireHeight, nil } -func (kp *Keeper) ExpireOrders( +func (kp *DexKeeper) ExpireOrders( ctx sdk.Context, blockTime int64, postAlloTransHandler TransferHandler, @@ -972,7 +865,7 @@ func (kp *Keeper) ExpireOrders( fees.Pool.AddAndCommitFee("EXPIRE", totalFee) } -func (kp *Keeper) MarkBreatheBlock(ctx sdk.Context, height int64, blockTime time.Time) { +func (kp *DexKeeper) MarkBreatheBlock(ctx sdk.Context, height int64, blockTime time.Time) { key := utils.Int642Bytes(blockTime.Unix() / utils.SecondsPerDay) store := ctx.KVStore(kp.storeKey) bz, err := kp.cdc.MarshalBinaryBare(height) @@ -983,7 +876,7 @@ func (kp *Keeper) MarkBreatheBlock(ctx sdk.Context, height int64, blockTime time store.Set([]byte(key), bz) } -func (kp *Keeper) GetBreatheBlockHeight(ctx sdk.Context, timeNow time.Time, daysBack int) (int64, error) { +func (kp *DexKeeper) GetBreatheBlockHeight(ctx sdk.Context, timeNow time.Time, daysBack int) (int64, error) { store := ctx.KVStore(kp.storeKey) t := timeNow.AddDate(0, 0, -daysBack).Unix() day := t / utils.SecondsPerDay @@ -1000,7 +893,7 @@ func (kp *Keeper) GetBreatheBlockHeight(ctx sdk.Context, timeNow time.Time, days return height, nil } -func (kp *Keeper) GetLastBreatheBlockHeight(ctx sdk.Context, latestBlockHeight int64, timeNow time.Time, blockInterval, daysBack int) int64 { +func (kp *DexKeeper) GetLastBreatheBlockHeight(ctx sdk.Context, latestBlockHeight int64, timeNow time.Time, blockInterval, daysBack int) int64 { if blockInterval != 0 { return (latestBlockHeight / int64(blockInterval)) * int64(blockInterval) } else { @@ -1031,7 +924,7 @@ func (kp *Keeper) GetLastBreatheBlockHeight(ctx sdk.Context, latestBlockHeight i // deliberately make `fee` parameter not a pointer // in case we modify the original fee (which will be referenced when distribute to validator) -func (kp *Keeper) updateRoundOrderFee(addr string, fee types.Fee) { +func (kp *DexKeeper) updateRoundOrderFee(addr string, fee types.Fee) { if existingFee, ok := kp.RoundOrderFees[addr]; ok { existingFee.AddFee(fee) } else { @@ -1039,20 +932,47 @@ func (kp *Keeper) updateRoundOrderFee(addr string, fee types.Fee) { } } -func (kp *Keeper) ClearRoundFee() { +func (kp *DexKeeper) ClearRoundFee() { kp.RoundOrderFees = make(map[string]*types.Fee, 256) } -// used by state sync to clear memory order book after we synced latest breathe block -func (kp *Keeper) ClearOrders() { - kp.engines = make(map[string]*me.MatchEng) - kp.allOrders = make(map[string]map[string]*OrderInfo, 256) - kp.roundOrders = make(map[string][]string, 256) - kp.roundIOCOrders = make(map[string][]string, 256) - kp.OrderInfosForPub = make(OrderInfoForPublish) +func (kp *DexKeeper) CanDelistTradingPair(ctx sdk.Context, baseAsset, quoteAsset string) error { + // trading pair against native token should not be delisted if there is any other trading pair exist + baseAsset = strings.ToUpper(baseAsset) + quoteAsset = strings.ToUpper(quoteAsset) + + if baseAsset == quoteAsset { + return fmt.Errorf("base asset symbol should not be identical to quote asset symbol") + } + + if !kp.PairMapper.Exists(ctx, baseAsset, quoteAsset) { + return fmt.Errorf("trading pair %s_%s does not exist", baseAsset, quoteAsset) + } + + if baseAsset != types.NativeTokenSymbol && quoteAsset != types.NativeTokenSymbol { + return nil + } + + var symbolToCheck string + if baseAsset != types.NativeTokenSymbol { + symbolToCheck = baseAsset + } else { + symbolToCheck = quoteAsset + } + + tradingPairs := kp.PairMapper.ListAllTradingPairs(ctx) + for _, pair := range tradingPairs { //TODO + if (pair.BaseAssetSymbol == symbolToCheck && pair.QuoteAssetSymbol != types.NativeTokenSymbol) || + (pair.QuoteAssetSymbol == symbolToCheck && pair.BaseAssetSymbol != types.NativeTokenSymbol) { + return fmt.Errorf("trading pair %s_%s should not exist before delisting %s_%s", + pair.BaseAssetSymbol, pair.QuoteAssetSymbol, baseAsset, quoteAsset) + } + } + + return nil } -func (kp *Keeper) DelistTradingPair(ctx sdk.Context, symbol string, postAllocTransHandler TransferHandler) { +func (kp *DexKeeper) DelistTradingPair(ctx sdk.Context, symbol string, postAllocTransHandler TransferHandler) { _, ok := kp.engines[symbol] if !ok { kp.logger.Error("delist symbol does not exist", "symbol", symbol) @@ -1066,8 +986,8 @@ func (kp *Keeper) DelistTradingPair(ctx sdk.Context, symbol string, postAllocTra } delete(kp.engines, symbol) - delete(kp.allOrders, symbol) delete(kp.recentPrices, symbol) + kp.mustGetOrderKeeper(symbol).deleteOrdersForPair(symbol) baseAsset, quoteAsset := dexUtils.TradingPair2AssetsSafe(symbol) err := kp.PairMapper.DeleteTradingPair(ctx, baseAsset, quoteAsset) @@ -1076,8 +996,13 @@ func (kp *Keeper) DelistTradingPair(ctx sdk.Context, symbol string, postAllocTra } } -func (kp *Keeper) expireAllOrders(ctx sdk.Context, symbol string) []chan Transfer { - orderNum := len(kp.allOrders[symbol]) +func (kp *DexKeeper) expireAllOrders(ctx sdk.Context, symbol string) []chan Transfer { + ordersOfSymbol := make(map[string]*OrderInfo) + if dexOrderKeeper, err := kp.getOrderKeeper(symbol); err == nil { + ordersOfSymbol = dexOrderKeeper.getAllOrdersForPair(symbol) + } + + orderNum := len(ordersOfSymbol) if orderNum == 0 { kp.logger.Info("no orders to expire", "symbol", symbol) return nil @@ -1105,7 +1030,7 @@ func (kp *Keeper) expireAllOrders(ctx sdk.Context, symbol string) []chan Transfe go func() { engine := kp.engines[symbol] - orders := kp.allOrders[symbol] + orders := ordersOfSymbol expire(orders, engine, me.BUYSIDE) expire(orders, engine, me.SELLSIDE) @@ -1117,7 +1042,7 @@ func (kp *Keeper) expireAllOrders(ctx sdk.Context, symbol string) []chan Transfe return transferChs } -func (kp *Keeper) CanListTradingPair(ctx sdk.Context, baseAsset, quoteAsset string) error { +func (kp *DexKeeper) CanListTradingPair(ctx sdk.Context, baseAsset, quoteAsset string) error { // trading pair against native token should exist if quote token is not native token baseAsset = strings.ToUpper(baseAsset) quoteAsset = strings.ToUpper(quoteAsset) @@ -1133,7 +1058,7 @@ func (kp *Keeper) CanListTradingPair(ctx sdk.Context, baseAsset, quoteAsset stri if baseAsset != types.NativeTokenSymbol && quoteAsset != types.NativeTokenSymbol { - // support busd pair listing + // support busd pair listing including mini-token as base if sdk.IsUpgrade(upgrade.BEP70) && len(BUSDSymbol) > 0 { if baseAsset == BUSDSymbol || quoteAsset == BUSDSymbol { if kp.pairExistsBetween(ctx, types.NativeTokenSymbol, BUSDSymbol) { @@ -1152,46 +1077,119 @@ func (kp *Keeper) CanListTradingPair(ctx sdk.Context, baseAsset, quoteAsset stri quoteAsset, baseAsset, quoteAsset) } } + return nil } -func (kp *Keeper) CanDelistTradingPair(ctx sdk.Context, baseAsset, quoteAsset string) error { - // trading pair against native token should not be delisted if there is any other trading pair exist - baseAsset = strings.ToUpper(baseAsset) - quoteAsset = strings.ToUpper(quoteAsset) +func (kp *DexKeeper) GetAllOrders() map[string]map[string]*OrderInfo { + allOrders := make(map[string]map[string]*OrderInfo) + for _, orderKeeper := range kp.OrderKeepers { + allOrders = appendAllOrdersMap(allOrders, orderKeeper.getAllOrders()) + } + return allOrders +} - if baseAsset == quoteAsset { - return fmt.Errorf("base asset symbol should not be identical to quote asset symbol") +// ONLY FOR TEST USE +func (kp *DexKeeper) GetAllOrdersForPair(symbol string) map[string]*OrderInfo { + return kp.mustGetOrderKeeper(symbol).getAllOrdersForPair(symbol) +} + +func (kp *DexKeeper) ReloadOrder(symbol string, orderInfo *OrderInfo, height int64) { + kp.mustGetOrderKeeper(symbol).reloadOrder(symbol, orderInfo, height) +} + +func (kp *DexKeeper) GetOrderChanges(pairType SymbolPairType) OrderChanges { + for _, orderKeeper := range kp.OrderKeepers { + if orderKeeper.supportPairType(pairType) { + return orderKeeper.getOrderChanges() + } + } + kp.logger.Error("pairType is not supported %d for OrderChanges", pairType) + return make(OrderChanges, 0) +} +func (kp *DexKeeper) GetAllOrderChanges() OrderChanges { + var res OrderChanges + for _, orderKeeper := range kp.OrderKeepers { + if orderKeeper.supportUpgradeVersion() { + res = append(res, orderKeeper.getOrderChanges()...) + } } + return res +} - if !kp.PairMapper.Exists(ctx, baseAsset, quoteAsset) { - return fmt.Errorf("trading pair %s_%s does not exist", baseAsset, quoteAsset) +func (kp *DexKeeper) UpdateOrderChangeSync(change OrderChange, symbol string) { + if dexOrderKeeper, err := kp.getOrderKeeper(symbol); err == nil { + dexOrderKeeper.appendOrderChangeSync(change) + return } + kp.logger.Error("symbol is not supported %d", symbol) +} - if baseAsset != types.NativeTokenSymbol && quoteAsset != types.NativeTokenSymbol { - return nil +func (kp *DexKeeper) GetOrderInfosForPub(pairType SymbolPairType) OrderInfoForPublish { + for _, orderKeeper := range kp.OrderKeepers { + if orderKeeper.supportPairType(pairType) { + return orderKeeper.getOrderInfosForPub() + } } + kp.logger.Error("pairType is not supported %d for OrderInfosForPub", pairType) + return make(OrderInfoForPublish) +} - var symbolToCheck string - if baseAsset != types.NativeTokenSymbol { - symbolToCheck = baseAsset - } else { - symbolToCheck = quoteAsset +func (kp *DexKeeper) GetAllOrderInfosForPub() OrderInfoForPublish { + orderInfoForPub := make(OrderInfoForPublish) + for _, orderKeeper := range kp.OrderKeepers { + if orderKeeper.supportUpgradeVersion() { + orderInfoForPub = appendOrderInfoForPub(orderInfoForPub, orderKeeper.getOrderInfosForPub()) + } } + return orderInfoForPub +} - tradingPairs := kp.PairMapper.ListAllTradingPairs(ctx) - for _, pair := range tradingPairs { - if (pair.BaseAssetSymbol == symbolToCheck && pair.QuoteAssetSymbol != types.NativeTokenSymbol) || - (pair.QuoteAssetSymbol == symbolToCheck && pair.BaseAssetSymbol != types.NativeTokenSymbol) { - return fmt.Errorf("trading pair %s_%s should not exist before delisting %s_%s", - pair.BaseAssetSymbol, pair.QuoteAssetSymbol, baseAsset, quoteAsset) +func (kp *DexKeeper) RemoveOrderInfosForPub(pair string, orderId string) { + if orderKeeper, err := kp.getOrderKeeper(pair); err == nil { + orderKeeper.removeOrderInfosForPub(orderId) + return + } + + kp.logger.Error("pair is not supported %d", pair) +} + +func (kp *DexKeeper) ShouldPublishOrder() bool { + return kp.CollectOrderInfoForPublish +} + +func (kp *DexKeeper) GetEngines() map[string]*me.MatchEng { + return kp.engines +} +func appendAllOrdersMap(ms ...map[string]map[string]*OrderInfo) map[string]map[string]*OrderInfo { + res := make(map[string]map[string]*OrderInfo) + for _, m := range ms { + for k, v := range m { + res[k] = v } } + return res +} - return nil +func appendOrderInfoForPub(ms ...OrderInfoForPublish) OrderInfoForPublish { + res := make(OrderInfoForPublish) + for _, m := range ms { + for k, v := range m { + res[k] = v + } + } + return res +} + +func CreateMatchEng(pairSymbol string, basePrice, lotSize int64) *me.MatchEng { + return me.NewMatchEng(pairSymbol, basePrice, lotSize, 0.05) +} + +func isMiniSymbolPair(baseAsset, quoteAsset string) bool { + return types.IsMiniTokenSymbol(baseAsset) || types.IsMiniTokenSymbol(quoteAsset) } // Check whether there is trading pair between two symbols -func (kp *Keeper) pairExistsBetween(ctx sdk.Context, symbolA, symbolB string) bool { +func (kp *DexKeeper) pairExistsBetween(ctx sdk.Context, symbolA, symbolB string) bool { return kp.PairMapper.Exists(ctx, symbolA, symbolB) || kp.PairMapper.Exists(ctx, symbolB, symbolA) } diff --git a/plugins/dex/order/keeper_match.go b/plugins/dex/order/keeper_match.go new file mode 100644 index 000000000..0b439e664 --- /dev/null +++ b/plugins/dex/order/keeper_match.go @@ -0,0 +1,178 @@ +package order + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/binance-chain/node/common/fees" + "github.com/binance-chain/node/common/utils" +) + +func (kp *DexKeeper) SelectSymbolsToMatch(height int64, matchAllSymbols bool) []string { + symbolsToMatch := make([]string, 0, 256) + for _, orderKeeper := range kp.OrderKeepers { + if orderKeeper.supportUpgradeVersion() { + symbolsToMatch = append(symbolsToMatch, orderKeeper.selectSymbolsToMatch(height, matchAllSymbols)...) + } + } + return symbolsToMatch +} + +func (kp *DexKeeper) MatchAndAllocateSymbols(ctx sdk.Context, postAlloTransHandler TransferHandler, matchAllSymbols bool) { + kp.logger.Debug("Start Matching for all...", "height", ctx.BlockHeader().Height) + blockHeader := ctx.BlockHeader() + timestamp := blockHeader.Time.UnixNano() + + symbolsToMatch := kp.SelectSymbolsToMatch(blockHeader.Height, matchAllSymbols) + kp.logger.Info("symbols to match", "symbols", symbolsToMatch) + var tradeOuts []chan Transfer + if len(symbolsToMatch) == 0 { + kp.logger.Info("No order comes in for the block") + } else { + tradeOuts = kp.matchAndDistributeTrades(true, blockHeader.Height, timestamp) + } + + totalFee := kp.allocateAndCalcFee(ctx, tradeOuts, postAlloTransHandler) + fees.Pool.AddAndCommitFee("MATCH", totalFee) + kp.ClearAfterMatch() +} + +// please note if distributeTrade this method will work in async mode, otherwise in sync mode. +// Always run kp.SelectSymbolsToMatch(ctx.BlockHeader().Height, matchAllSymbols) before matchAndDistributeTrades +func (kp *DexKeeper) matchAndDistributeTrades(distributeTrade bool, height, timestamp int64) []chan Transfer { + concurrency := 1 << kp.poolSize + tradeOuts := make([]chan Transfer, concurrency) + + if distributeTrade { + ordNum := 0 + for i := range kp.OrderKeepers { + ordNum += kp.OrderKeepers[i].getRoundOrdersNum() + } + + for i := range tradeOuts { + //assume every new order would have 2 trades and generate 4 transfer + tradeOuts[i] = make(chan Transfer, ordNum*4/concurrency) + } + } + + symbolCh := make(chan string, concurrency) + producer := func() { + for i := range kp.OrderKeepers { + kp.OrderKeepers[i].iterateRoundSelectedPairs(func(symbol string) { + symbolCh <- symbol + }) + } + close(symbolCh) + } + matchWorker := func() { + for symbol := range symbolCh { + kp.matchAndDistributeTradesForSymbol(symbol, height, timestamp, distributeTrade, tradeOuts) + } + + } + + if distributeTrade { + utils.ConcurrentExecuteAsync(concurrency, producer, matchWorker, func() { + for _, tradeOut := range tradeOuts { + close(tradeOut) + } + }) + } else { + utils.ConcurrentExecuteSync(concurrency, producer, matchWorker) + } + return tradeOuts +} + +func (kp *DexKeeper) MatchSymbols(height, timestamp int64, matchAllSymbols bool) { + symbolsToMatch := kp.SelectSymbolsToMatch(height, matchAllSymbols) + kp.logger.Debug("symbols to match", "symbols", symbolsToMatch) + + if len(symbolsToMatch) == 0 { + kp.logger.Info("No order comes in for the block") + } else { + kp.matchAndDistributeTrades(false, height, timestamp) + } + + kp.ClearAfterMatch() +} + +func (kp *DexKeeper) matchAndDistributeTradesForSymbol(symbol string, height, timestamp int64, distributeTrade bool, + tradeOuts []chan Transfer) { + engine := kp.engines[symbol] + concurrency := len(tradeOuts) + orderKeeper := kp.mustGetOrderKeeper(symbol) + orders := orderKeeper.getAllOrdersForPair(symbol) + // please note there is no logging in matching, expecting to see the order book details + // from the exchange's order book stream. + if engine.Match(height) { + kp.logger.Debug("Match finish:", "symbol", symbol, "lastTradePrice", engine.LastTradePrice) + for i := range engine.Trades { + t := &engine.Trades[i] + updateOrderMsg(orders[t.Bid], t.BuyCumQty, height, timestamp) + updateOrderMsg(orders[t.Sid], t.SellCumQty, height, timestamp) + if distributeTrade { + t1, t2 := TransferFromTrade(t, symbol, orders) + c := channelHash(t1.accAddress, concurrency) + tradeOuts[c] <- t1 + c = channelHash(t2.accAddress, concurrency) + tradeOuts[c] <- t2 + } + } + droppedIds := engine.DropFilledOrder() //delete from order books + for _, id := range droppedIds { + delete(orders, id) //delete from order cache + } + kp.logger.Debug("Drop filled orders", "total", droppedIds) + } else { + // FUTURE-TODO: + // when Match() failed, have to unsolicited cancel all the new orders + // in this block. Ideally the order IDs would be stored in the EndBlock response, + // but this is not implemented yet, pending Tendermint to better handle EndBlock + // for index service. + kp.logger.Error("Fatal error occurred in matching, cancel all incoming new orders", + "symbol", symbol) + thisRoundIds := orderKeeper.getRoundOrdersForPair(symbol) + for _, id := range thisRoundIds { + msg := orders[id] + delete(orders, id) + if ord, err := engine.Book.RemoveOrder(id, msg.Side, msg.Price); err == nil { + kp.logger.Info("Removed due to match failure", "ordID", msg.Id) + if distributeTrade { + c := channelHash(msg.Sender, concurrency) + tradeOuts[c] <- TransferFromCanceled(ord, *msg, true) + } + } else { + kp.logger.Error("Failed to remove order, may be fatal!", "orderID", id) + } + + // let the order status publisher publish these abnormal + // order status change outs. + if kp.CollectOrderInfoForPublish { + orderKeeper.appendOrderChangeSync(OrderChange{id, FailedMatching, "", nil}) + } + } + return // no need to handle IOC + } + var iocIDs []string + iocIDs = orderKeeper.getRoundIOCOrdersForPair(symbol) + for _, id := range iocIDs { + if msg, ok := orders[id]; ok { + delete(orders, id) + if ord, err := engine.Book.RemoveOrder(id, msg.Side, msg.Price); err == nil { + kp.logger.Debug("Removed unclosed IOC order", "ordID", msg.Id) + if distributeTrade { + c := channelHash(msg.Sender, concurrency) + tradeOuts[c] <- TransferFromExpired(ord, *msg) + } + } else { + kp.logger.Error("Failed to remove IOC order, may be fatal!", "orderID", id) + } + } + } +} + +// Run as postConsume procedure of async, no concurrent updates of orders map +func updateOrderMsg(order *OrderInfo, cumQty, height, timestamp int64) { + order.CumQty = cumQty + order.LastUpdatedHeight = height + order.LastUpdatedTimestamp = timestamp +} diff --git a/plugins/dex/order/keeper_recovery.go b/plugins/dex/order/keeper_recovery.go index 1ce420e1c..860e6dc82 100644 --- a/plugins/dex/order/keeper_recovery.go +++ b/plugins/dex/order/keeper_recovery.go @@ -28,9 +28,10 @@ import ( ) type OrderBookSnapshot struct { - Buys []me.PriceLevel `json:"buys"` - Sells []me.PriceLevel `json:"sells"` - LastTradePrice int64 `json:"lasttradeprice"` + Buys []me.PriceLevel `json:"buys"` + Sells []me.PriceLevel `json:"sells"` + LastTradePrice int64 `json:"lasttradeprice"` + LastMatchHeight int64 `json:"lastmatchheight"` } type ActiveOrders struct { @@ -59,12 +60,16 @@ func compressAndSave(snapshot interface{}, cdc *wire.Codec, key string, kv sdk.K return nil } -func (kp *Keeper) SnapShotOrderBook(ctx sdk.Context, height int64) (effectedStoreKeys []string, err error) { +func (kp *DexKeeper) SnapShotOrderBook(ctx sdk.Context, height int64) (effectedStoreKeys []string, err error) { kvstore := ctx.KVStore(kp.storeKey) effectedStoreKeys = make([]string, 0) for pair, eng := range kp.engines { buys, sells := eng.Book.GetAllLevels() - snapshot := OrderBookSnapshot{Buys: buys, Sells: sells, LastTradePrice: eng.LastTradePrice} + var snapshot OrderBookSnapshot + snapshot = OrderBookSnapshot{Buys: buys, Sells: sells, LastTradePrice: eng.LastTradePrice} + if sdk.IsUpgrade(upgrade.BEP8) { + snapshot.LastMatchHeight = eng.LastMatchHeight + } key := genOrderBookSnapshotKey(height, pair) effectedStoreKeys = append(effectedStoreKeys, key) err := compressAndSave(snapshot, kp.cdc, key, kvstore) @@ -74,17 +79,19 @@ func (kp *Keeper) SnapShotOrderBook(ctx sdk.Context, height int64) (effectedStor ctx.Logger().Info("Compressed and Saved order book snapshot", "pair", pair) } - msgs := make([]NewOrderMsg, len(kp.allOrders), len(kp.allOrders)) - msgKeys := make([]string, len(kp.allOrders), len(kp.allOrders)) - i := 0 - for key := range kp.allOrders { - msgKeys[i] = key - i++ + msgKeys := make([]string, 0) + idSymbolMap := make(map[string]string) + allOrders := kp.GetAllOrders() + for symbol, orderMap := range allOrders { + for id := range orderMap { + idSymbolMap[id] = symbol + msgKeys = append(msgKeys, id) + } } sort.Strings(msgKeys) msgs := make([]OrderInfo, len(msgKeys), len(msgKeys)) for i, key := range msgKeys { - msgs[i] = *kp.allOrders[idSymbolMap[key]][key] + msgs[i] = *allOrders[idSymbolMap[key]][key] } snapshot := ActiveOrders{Orders: msgs} @@ -94,7 +101,7 @@ func (kp *Keeper) SnapShotOrderBook(ctx sdk.Context, height int64) (effectedStor return effectedStoreKeys, compressAndSave(snapshot, kp.cdc, key, kvstore) } -func (kp *Keeper) LoadOrderBookSnapshot(ctx sdk.Context, latestBlockHeight int64, timeOfLatestBlock time.Time, blockInterval, daysBack int) (int64, error) { +func (kp *DexKeeper) LoadOrderBookSnapshot(ctx sdk.Context, latestBlockHeight int64, timeOfLatestBlock time.Time, blockInterval, daysBack int) (int64, error) { height := kp.GetLastBreatheBlockHeight(ctx, latestBlockHeight, timeOfLatestBlock, blockInterval, daysBack) ctx.Logger().Info("Loading order book snapshot from last breathe block", "blockHeight", height) allPairs := kp.PairMapper.ListAllTradingPairs(ctx) @@ -110,6 +117,7 @@ func (kp *Keeper) LoadOrderBookSnapshot(ctx sdk.Context, latestBlockHeight int64 return height, nil } + upgrade.Mgr.SetHeight(height) kvStore := ctx.KVStore(kp.storeKey) for _, pair := range allPairs { symbol := pair.GetSymbol() @@ -144,6 +152,11 @@ func (kp *Keeper) LoadOrderBookSnapshot(ctx sdk.Context, latestBlockHeight int64 eng.Book.InsertPriceLevel(&pl, me.SELLSIDE) } eng.LastTradePrice = ob.LastTradePrice + if sdk.IsUpgrade(upgrade.BEP8) { + eng.LastMatchHeight = ob.LastMatchHeight + } else { + eng.LastMatchHeight = height + } ctx.Logger().Info("Successfully Loaded order snapshot", "pair", pair) } key := genActiveOrdersSnapshotKey(height) @@ -167,25 +180,13 @@ func (kp *Keeper) LoadOrderBookSnapshot(ctx sdk.Context, latestBlockHeight int64 for _, m := range ao.Orders { orderHolder := m symbol := strings.ToUpper(m.Symbol) - kp.allOrders[symbol][m.Id] = &orderHolder - if m.CreatedHeight == height { - kp.roundOrders[symbol] = append(kp.roundOrders[symbol], m.Id) - if m.TimeInForce == TimeInForce.IOC { - kp.roundIOCOrders[symbol] = append(kp.roundIOCOrders[symbol], m.Id) - } - } - if kp.CollectOrderInfoForPublish { - if _, exists := kp.OrderInfosForPub[m.Id]; !exists { - bnclog.Debug("add order to order changes map, during load snapshot, from active orders", "orderId", m.Id) - kp.OrderInfosForPub[m.Id] = &orderHolder - } - } + kp.ReloadOrder(symbol, &orderHolder, height) } ctx.Logger().Info("Recovered active orders. Snapshot is fully loaded") return height, nil } -func (kp *Keeper) replayOneBlocks(logger log.Logger, block *tmtypes.Block, stateDB dbm.DB, txDecoder sdk.TxDecoder, +func (kp *DexKeeper) replayOneBlocks(logger log.Logger, block *tmtypes.Block, stateDB dbm.DB, txDecoder sdk.TxDecoder, height int64, timestamp time.Time) { if block == nil { logger.Error("No block is loaded. Ignore replay for orderbook") @@ -233,7 +234,7 @@ func (kp *Keeper) replayOneBlocks(logger log.Logger, block *tmtypes.Block, state err := kp.RemoveOrder(msg.RefId, msg.Symbol, func(ord me.OrderPart) { if kp.CollectOrderInfoForPublish { bnclog.Debug("deleted order from order changes map", "orderId", msg.RefId, "isRecovery", true) - delete(kp.OrderInfosForPub, msg.RefId) + kp.RemoveOrderInfosForPub(msg.Symbol, msg.RefId) } }) if err != nil { @@ -244,10 +245,10 @@ func (kp *Keeper) replayOneBlocks(logger log.Logger, block *tmtypes.Block, state } } logger.Info("replayed all tx. Starting match", "height", height) - kp.MatchAll(height, t) //no need to check result + kp.MatchSymbols(height, t, false) //no need to check result } -func (kp *Keeper) ReplayOrdersFromBlock(ctx sdk.Context, bc *tmstore.BlockStore, stateDb dbm.DB, lastHeight, breatheHeight int64, +func (kp *DexKeeper) ReplayOrdersFromBlock(ctx sdk.Context, bc *tmstore.BlockStore, stateDb dbm.DB, lastHeight, breatheHeight int64, txDecoder sdk.TxDecoder) error { for i := breatheHeight + 1; i <= lastHeight; i++ { block := bc.LoadBlock(i) @@ -258,7 +259,7 @@ func (kp *Keeper) ReplayOrdersFromBlock(ctx sdk.Context, bc *tmstore.BlockStore, return nil } -func (kp *Keeper) initOrderBook(ctx sdk.Context, blockInterval, daysBack int, blockStore *tmstore.BlockStore, stateDB dbm.DB, lastHeight int64, txDecoder sdk.TxDecoder) { +func (kp *DexKeeper) initOrderBook(ctx sdk.Context, blockInterval, daysBack int, blockStore *tmstore.BlockStore, stateDB dbm.DB, lastHeight int64, txDecoder sdk.TxDecoder) { var timeOfLatestBlock time.Time if lastHeight == 0 { timeOfLatestBlock = utils.Now() @@ -271,7 +272,7 @@ func (kp *Keeper) initOrderBook(ctx sdk.Context, blockInterval, daysBack int, bl panic(err) } logger := ctx.Logger().With("module", "dex") - logger.Info("Initialized Block Store for replay", "toHeight", lastHeight) + logger.Info("Initialized Block Store for replay", "fromHeight", height, "toHeight", lastHeight) err = kp.ReplayOrdersFromBlock(ctx.WithLogger(logger), blockStore, stateDB, lastHeight, height, txDecoder) if err != nil { panic(err) diff --git a/plugins/dex/order/keeper_test.go b/plugins/dex/order/keeper_test.go index f03e8c347..7dbeb4b02 100644 --- a/plugins/dex/order/keeper_test.go +++ b/plugins/dex/order/keeper_test.go @@ -52,12 +52,12 @@ func MakeCodec() *wire.Codec { return cdc } -func MakeKeeper(cdc *wire.Codec) *Keeper { - accountMapper := auth.NewAccountMapper(cdc, common.AccountStoreKey, types.ProtoAppAccount) +func MakeKeeper(cdc *wire.Codec) *DexKeeper { + accKeeper := auth.NewAccountKeeper(cdc, common.AccountStoreKey, types.ProtoAppAccount) codespacer := sdk.NewCodespacer() pairMapper := store.NewTradingPairMapper(cdc, common.PairStoreKey) - keeper := NewKeeper(common.DexStoreKey, accKeeper, pairMapper, - codespacer.RegisterNext(dextypes.DefaultCodespace), 2, cdc, true) + keeper := NewDexKeeper(common.DexStoreKey, accKeeper, pairMapper, codespacer.RegisterNext(dextypes.DefaultCodespace), 2, cdc, true) + return keeper } @@ -107,6 +107,8 @@ func TestKeeper_MatchFailure(t *testing.T) { msg = NewNewOrderMsg(accAdd, "123462", Side.BUY, "XYZ-000_BNB", 99000, 15000000) ord = OrderInfo{msg, 42, 0, 42, 0, 0, "", 0} keeper.AddOrder(ord, false) + symbolsToMatch := keeper.SelectSymbolsToMatch(ctx.BlockHeader().Height, false) + logger.Info("symbols to match", "symbols", symbolsToMatch) tradeOuts := keeper.matchAndDistributeTrades(true, 42, 0) c := channelHash(accAdd, 4) i := 0 @@ -182,7 +184,7 @@ func MakeAddress() (sdk.AccAddress, secp256k1.PrivKeySecp256k1) { return addr, privKey } -func effectedStoredKVPairs(keeper *Keeper, ctx sdk.Context, keys []string) map[string][]byte { +func effectedStoredKVPairs(keeper *DexKeeper, ctx sdk.Context, keys []string) map[string][]byte { res := make(map[string][]byte, len(keys)) store := ctx.KVStore(keeper.storeKey) for _, key := range keys { @@ -217,8 +219,8 @@ func TestKeeper_SnapShotOrderBook(t *testing.T) { keeper.AddOrder(OrderInfo{msg, 42, 84, 42, 84, 0, "", 0}, false) msg = NewNewOrderMsg(accAdd, "123462", Side.BUY, "XYZ-000_BNB", 96000, 1500000) keeper.AddOrder(OrderInfo{msg, 42, 84, 42, 84, 0, "", 0}, false) - assert.Equal(1, len(keeper.allOrders)) - assert.Equal(7, len(keeper.allOrders["XYZ-000_BNB"])) + assert.Equal(1, len(keeper.GetAllOrders())) + assert.Equal(7, len(keeper.GetAllOrdersForPair("XYZ-000_BNB"))) assert.Equal(1, len(keeper.engines)) effectedStoredKeys1, err := keeper.SnapShotOrderBook(ctx, 43) @@ -231,8 +233,8 @@ func TestKeeper_SnapShotOrderBook(t *testing.T) { keeper.MarkBreatheBlock(ctx, 43, time.Now()) keeper2 := MakeKeeper(cdc) h, err := keeper2.LoadOrderBookSnapshot(ctx, 43, utils.Now(), 0, 10) - assert.Equal(7, len(keeper2.allOrders["XYZ-000_BNB"])) - o123459 := keeper2.allOrders["XYZ-000_BNB"]["123459"] + assert.Equal(7, len(keeper2.GetAllOrdersForPair("XYZ-000_BNB"))) + o123459 := keeper2.GetAllOrdersForPair("XYZ-000_BNB")["123459"] assert.Equal(int64(98000), o123459.Price) assert.Equal(int64(1000000), o123459.Quantity) assert.Equal(int64(0), o123459.CumQty) @@ -271,25 +273,25 @@ func TestKeeper_SnapShotAndLoadAfterMatch(t *testing.T) { keeper.AddOrder(info123457, false) msg := NewNewOrderMsg(accAdd, "123458", Side.SELL, "XYZ-000_BNB", 100000, 2000000) keeper.AddOrder(OrderInfo{msg, 42, 0, 42, 0, 0, "", 0}, false) - assert.Equal(1, len(keeper.allOrders)) - assert.Equal(3, len(keeper.allOrders["XYZ-000_BNB"])) + assert.Equal(1, len(keeper.GetAllOrders())) + assert.Equal(3, len(keeper.GetAllOrdersForPair("XYZ-000_BNB"))) assert.Equal(1, len(keeper.engines)) - keeper.MatchAll(42, 0) + keeper.MatchSymbols(42, 0, false) _, err := keeper.SnapShotOrderBook(ctx, 43) assert.Nil(err) keeper.MarkBreatheBlock(ctx, 43, time.Now()) keeper2 := MakeKeeper(cdc) h, err := keeper2.LoadOrderBookSnapshot(ctx, 43, utils.Now(), 0, 10) - assert.Equal(2, len(keeper2.allOrders["XYZ-000_BNB"])) - assert.Equal(int64(102000), keeper2.allOrders["XYZ-000_BNB"]["123456"].Price) - assert.Equal(int64(2000000), keeper2.allOrders["XYZ-000_BNB"]["123456"].CumQty) - assert.Equal(int64(10000), keeper2.allOrders["XYZ-000_BNB"]["123457"].Price) - assert.Equal(int64(0), keeper2.allOrders["XYZ-000_BNB"]["123457"].CumQty) + assert.Equal(2, len(keeper2.GetAllOrdersForPair("XYZ-000_BNB"))) + assert.Equal(int64(102000), keeper2.GetAllOrdersForPair("XYZ-000_BNB")["123456"].Price) + assert.Equal(int64(2000000), keeper2.GetAllOrdersForPair("XYZ-000_BNB")["123456"].CumQty) + assert.Equal(int64(10000), keeper2.GetAllOrdersForPair("XYZ-000_BNB")["123457"].Price) + assert.Equal(int64(0), keeper2.GetAllOrdersForPair("XYZ-000_BNB")["123457"].CumQty) info123456_Changed := info123456 info123456_Changed.CumQty = 2000000 - assert.Equal(info123456_Changed, *keeper2.OrderInfosForPub["123456"]) - assert.Equal(info123457, *keeper2.OrderInfosForPub["123457"]) + assert.Equal(info123456_Changed, *keeper2.GetOrderInfosForPub(PairType.BEP2)["123456"]) + assert.Equal(info123457, *keeper2.GetOrderInfosForPub(PairType.BEP2)["123457"]) assert.Equal(1, len(keeper2.engines)) assert.Equal(int64(102000), keeper2.engines["XYZ-000_BNB"].LastTradePrice) assert.Equal(int64(43), h) @@ -325,7 +327,7 @@ func TestKeeper_SnapShotOrderBookEmpty(t *testing.T) { keeper2 := MakeKeeper(cdc) h, err := keeper2.LoadOrderBookSnapshot(ctx, 43, utils.Now(), 0, 10) assert.Equal(int64(43), h) - assert.Equal(0, len(keeper2.allOrders["XYZ-000_BNB"])) + assert.Equal(0, len(keeper2.GetAllOrdersForPair("XYZ-000_BNB"))) buys, sells = keeper2.engines["XYZ-000_BNB"].Book.GetAllLevels() assert.Equal(0, len(buys)) assert.Equal(0, len(sells)) @@ -538,7 +540,7 @@ func getAccountCache(cdc *codec.Codec, ms sdk.MultiStore, accountKey *sdk.KVStor return auth.NewAccountCache(accountStoreCache) } -func setup() (ctx sdk.Context, mapper auth.AccountKeeper, keeper *Keeper) { +func setup() (ctx sdk.Context, mapper auth.AccountKeeper, keeper *DexKeeper) { ms, capKey, capKey2 := testutils.SetupMultiStoreForUnitTest() cdc := wire.NewCodec() types.RegisterWire(cdc) @@ -547,7 +549,8 @@ func setup() (ctx sdk.Context, mapper auth.AccountKeeper, keeper *Keeper) { accountCache := getAccountCache(cdc, ms, capKey) pairMapper := store.NewTradingPairMapper(cdc, common.PairStoreKey) ctx = sdk.NewContext(ms, abci.Header{ChainID: "mychainid"}, sdk.RunTxModeDeliver, log.NewNopLogger()).WithAccountCache(accountCache) - keeper = NewKeeper(capKey2, mapper, pairMapper, sdk.NewCodespacer().RegisterNext(dextypes.DefaultCodespace), 2, cdc, false) + + keeper = NewDexKeeper(capKey2, mapper, pairMapper, sdk.NewCodespacer().RegisterNext(dextypes.DefaultCodespace), 2, cdc, false) return } @@ -579,13 +582,13 @@ func TestKeeper_ExpireOrders(t *testing.T) { require.Len(t, sells, 1) require.Len(t, sells[0].Orders, 1) require.Equal(t, int64(2e8), sells[0].TotalLeavesQty()) - require.Len(t, keeper.allOrders["ABC-000_BNB"], 1) + require.Len(t, keeper.GetAllOrdersForPair("ABC-000_BNB"), 1) buys, sells = keeper.engines["XYZ-000_BNB"].Book.GetAllLevels() require.Len(t, buys, 1) require.Len(t, sells, 0) require.Len(t, buys[0].Orders, 1) require.Equal(t, int64(2e6), buys[0].TotalLeavesQty()) - require.Len(t, keeper.allOrders["XYZ-000_BNB"], 1) + require.Len(t, keeper.GetAllOrdersForPair("XYZ-000_BNB"), 1) expectFees := types.NewFee(sdk.Coins{ sdk.NewCoin("BNB", 6e4), sdk.NewCoin("ABC-000", 1e7), @@ -638,13 +641,13 @@ func TestKeeper_ExpireOrdersBasedOnPrice(t *testing.T) { require.Len(t, sells, 2) require.Len(t, sells[0].Orders, 1) require.Equal(t, int64(1e8), sells[0].TotalLeavesQty()) - require.Len(t, keeper.allOrders["ABC-000_BNB"], 4) + require.Len(t, keeper.GetAllOrdersForPair("ABC-000_BNB"), 4) buys, sells = keeper.engines["XYZ-000_BNB"].Book.GetAllLevels() require.Len(t, buys, 2) require.Len(t, sells, 0) require.Len(t, buys[0].Orders, 1) require.Equal(t, int64(2e6), buys[0].TotalLeavesQty()) - require.Len(t, keeper.allOrders["XYZ-000_BNB"], 2) + require.Len(t, keeper.GetAllOrdersForPair("XYZ-000_BNB"), 2) fees.Pool.Clear() upgrade.Mgr.AddUpgradeHeight(upgrade.BEP67, 0) } @@ -678,6 +681,42 @@ func TestKeeper_DetermineLotSize(t *testing.T) { assert.Equal(int64(1e5), lotsize) // wma price of BNB/BTC-000 is between 1e7 and 1e8 } +func TestKeeper_DetermineLotSize_SupportBUSD(t *testing.T) { + assert := assert.New(t) + ctx, _, keeper := setup() + + upgrade.Mgr.AddUpgradeHeight(upgrade.BEP70, -1) + keeper.SetBUSDSymbol("BUSD-BD1") + + // no recentPrices recorded, use engine.LastTradePrice + pair1 := dextypes.NewTradingPairWithLotSize("BNB", "BUSD-BD1", 1e6, 1e5) + keeper.AddEngine(pair1) + pair2 := dextypes.NewTradingPairWithLotSize("AAA-000", "BUSD-BD1", 1e6, 1e7) + keeper.AddEngine(pair2) + lotsize := keeper.DetermineLotSize("BNB", "BUSD-BD1", 1e6) + assert.Equal(int64(1e5), lotsize) + lotsize = keeper.DetermineLotSize("BUSD-BD1", "BNB", 1e10) + assert.Equal(int64(1e3), lotsize) + lotsize = keeper.DetermineLotSize("AAA-000", "BUSD-BD1", 1e6) + assert.Equal(int64(1e5), lotsize) + lotsize = keeper.DetermineLotSize("BUSD-BD1", "AAA-000", 1e10) + assert.Equal(int64(1e3), lotsize) + + // no trading yet, use list price + lotsize = keeper.DetermineLotSize("BBB-000", "BUSD-BD1", 1e8) + assert.Equal(int64(1e3), lotsize) + + // store some recentPrices + keeper.StoreTradePrices(ctx.WithBlockHeight(1 * pricesStoreEvery)) + keeper.engines[pair1.GetSymbol()].LastTradePrice = 1e8 + keeper.engines[pair2.GetSymbol()].LastTradePrice = 1e8 + keeper.StoreTradePrices(ctx.WithBlockHeight(2 * pricesStoreEvery)) + lotsize = keeper.DetermineLotSize("AAA-000", "BUSD-BD1", 1e4) + assert.Equal(int64(1e6), lotsize) // wma price of AAA-000/BNB is between 1e7 and 1e8 + lotsize = keeper.DetermineLotSize("BUSD-BD1", "AAA-000", 1e12) + assert.Equal(int64(1e5), lotsize) // wma price of BUSD-BD1/BNB is between 1e8 and 1e9 +} + func TestKeeper_UpdateTickSizeAndLotSize(t *testing.T) { assert := assert.New(t) ctx, _, keeper := setup() @@ -762,8 +801,7 @@ func TestOpenOrders_AfterMatch(t *testing.T) { assert.Equal(1, len(res)) // match existing two orders - matchRes, _ := keeper.MatchAll(43, 86) - assert.Equal(sdk.CodeOK, matchRes) + keeper.MatchSymbols(43, 86, false) // after match, the original buy order's cumQty and latest updated fields should be updated res = keeper.GetOpenOrders("NNB_BNB", zc) @@ -797,8 +835,7 @@ func TestOpenOrders_AfterMatch(t *testing.T) { assert.Equal(int64(88), res[0].LastUpdatedTimestamp) // match existing two orders - matchRes, _ = keeper.MatchAll(44, 88) - assert.Equal(sdk.CodeOK, matchRes) + keeper.MatchSymbols(44, 88, false) // after match, all orders should be closed res = keeper.GetOpenOrders("NNB_BNB", zc) @@ -848,12 +885,12 @@ func TestKeeper_DelistTradingPair(t *testing.T) { keeper.AddOrder(OrderInfo{msg, 42, 84, 42, 84, 0, "", 0}, false) msg = NewNewOrderMsg(addr, "123462", Side.BUY, "XYZ-000_BNB", 4e6, 1e6) keeper.AddOrder(OrderInfo{msg, 42, 84, 42, 84, 0, "", 0}, false) - assert.Equal(1, len(keeper.allOrders)) - assert.Equal(9, len(keeper.allOrders["XYZ-000_BNB"])) + assert.Equal(1, len(keeper.GetAllOrders())) + assert.Equal(9, len(keeper.GetAllOrdersForPair("XYZ-000_BNB"))) assert.Equal(1, len(keeper.engines)) keeper.DelistTradingPair(ctx, "XYZ-000_BNB", nil) - assert.Equal(0, len(keeper.allOrders)) + assert.Equal(0, len(keeper.GetAllOrders())) assert.Equal(0, len(keeper.engines)) expectFees := types.NewFee(sdk.Coins{ @@ -863,6 +900,64 @@ func TestKeeper_DelistTradingPair(t *testing.T) { require.Equal(t, expectFees, fees.Pool.BlockFees()) } +func TestKeeper_DelistMiniTradingPair(t *testing.T) { + setChainVersion() + defer resetChainVersion() + assert := assert.New(t) + ctx, am, keeper := setup() + fees.Pool.Clear() + keeper.FeeManager.UpdateConfig(NewTestFeeConfig()) + _, acc := testutils.NewAccount(ctx, am, 0) + addr := acc.GetAddress() + + tradingPair := dextypes.NewTradingPair("XYZ-000M", "BNB", 1e8) + keeper.PairMapper.AddTradingPair(ctx, tradingPair) + keeper.AddEngine(tradingPair) + + acc.(types.NamedAccount).SetLockedCoins(sdk.Coins{ + sdk.NewCoin("BNB", 11e4), + sdk.NewCoin("XYZ-000M", 4e4), + }.Sort()) + + acc.(types.NamedAccount).SetCoins(sdk.Coins{ + sdk.NewCoin("XYZ-000M", 4e5), + }.Sort()) + + am.SetAccount(ctx, acc) + + msg := NewNewOrderMsg(addr, "123456", Side.BUY, "XYZ-000M_BNB", 1e6, 1e6) + keeper.AddOrder(OrderInfo{msg, 42, 84, 42, 84, 0, "", 0}, false) + msg = NewNewOrderMsg(addr, "1234562", Side.BUY, "XYZ-000M_BNB", 1e6, 1e6) + keeper.AddOrder(OrderInfo{msg, 42, 84, 42, 84, 0, "", 0}, false) + msg = NewNewOrderMsg(addr, "123457", Side.BUY, "XYZ-000M_BNB", 2e6, 1e6) + keeper.AddOrder(OrderInfo{msg, 42, 84, 42, 84, 0, "", 0}, false) + msg = NewNewOrderMsg(addr, "123458", Side.BUY, "XYZ-000M_BNB", 3e6, 1e6) + keeper.AddOrder(OrderInfo{msg, 42, 84, 42, 84, 0, "", 0}, false) + msg = NewNewOrderMsg(addr, "123459", Side.SELL, "XYZ-000M_BNB", 5e6, 1e4) + keeper.AddOrder(OrderInfo{msg, 42, 84, 42, 84, 0, "", 0}, false) + msg = NewNewOrderMsg(addr, "123460", Side.SELL, "XYZ-000M_BNB", 6e6, 1e4) + keeper.AddOrder(OrderInfo{msg, 42, 84, 42, 84, 0, "", 0}, false) + msg = NewNewOrderMsg(addr, "1234602", Side.SELL, "XYZ-000M_BNB", 6e6, 1e4) + keeper.AddOrder(OrderInfo{msg, 42, 84, 42, 84, 0, "", 0}, false) + msg = NewNewOrderMsg(addr, "123461", Side.SELL, "XYZ-000M_BNB", 7e6, 1e4) + keeper.AddOrder(OrderInfo{msg, 42, 84, 42, 84, 0, "", 0}, false) + msg = NewNewOrderMsg(addr, "123462", Side.BUY, "XYZ-000M_BNB", 4e6, 1e6) + keeper.AddOrder(OrderInfo{msg, 42, 84, 42, 84, 0, "", 0}, false) + assert.Equal(1, len(keeper.GetAllOrders())) + assert.Equal(9, len(keeper.GetAllOrdersForPair("XYZ-000M_BNB"))) + assert.Equal(1, len(keeper.engines)) + + keeper.DelistTradingPair(ctx, "XYZ-000M_BNB", nil) + assert.Equal(0, len(keeper.GetAllOrders())) + assert.Equal(0, len(keeper.engines)) + + expectFees := types.NewFee(sdk.Coins{ + sdk.NewCoin("BNB", 10e4), + sdk.NewCoin("XYZ-000M", 4e5), + }.Sort(), types.FeeForProposer) + require.Equal(t, expectFees, fees.Pool.BlockFees()) +} + // func TestKeeper_DelistTradingPair_Empty(t *testing.T) { assert := assert.New(t) @@ -874,12 +969,12 @@ func TestKeeper_DelistTradingPair_Empty(t *testing.T) { keeper.PairMapper.AddTradingPair(ctx, tradingPair) keeper.AddEngine(tradingPair) - assert.Equal(1, len(keeper.allOrders)) - assert.Equal(0, len(keeper.allOrders["XYZ-001_BNB"])) + assert.Equal(1, len(keeper.GetAllOrders())) + assert.Equal(0, len(keeper.GetAllOrdersForPair("XYZ-001_BNB"))) assert.Equal(1, len(keeper.engines)) keeper.DelistTradingPair(ctx, "XYZ-001_BNB", nil) - assert.Equal(0, len(keeper.allOrders)) + assert.Equal(0, len(keeper.GetAllOrders())) assert.Equal(0, len(keeper.engines)) expectFees := types.NewFee(sdk.Coins(nil), types.ZeroFee) @@ -952,6 +1047,7 @@ func TestKeeper_CanListTradingPair_SupportBUSD(t *testing.T) { // upgraded, but BNB-BUSD pair does not exist upgrade.Mgr.AddUpgradeHeight(upgrade.BEP70, -1) + keeper.SetBUSDSymbol("BUSD-BD1") err = keeper.CanListTradingPair(ctx, "AAA-000", "BUSD-BD1") require.NotNil(t, err) require.Contains(t, err.Error(), "token AAA-000 should be listed against BNB before against BUSD-BD1") @@ -1012,3 +1108,39 @@ func TestKeeper_CanDelistTradingPair_SupportBUSD(t *testing.T) { err = keeper.CanDelistTradingPair(ctx, "AAA-000", "XYZ-000") require.Nil(t, err) } + +func TestKeeper_CanDelistMiniTradingPair_SupportBUSD(t *testing.T) { + ctx, _, keeper := setup() + err := keeper.PairMapper.AddTradingPair(ctx, dextypes.NewTradingPair("AAA-000M", "BUSD-BD1", 1e8)) + err = keeper.CanDelistTradingPair(ctx, "AAA-000M", "BUSD-BD1") + require.Nil(t, err) + + err = keeper.PairMapper.AddTradingPair(ctx, dextypes.NewTradingPair("BUSD-BD1", "AAA-000M", 1e8)) + err = keeper.CanDelistTradingPair(ctx, "BUSD-BD1", "AAA-000M") + require.Nil(t, err) + + // delisting AAA-XYZ will not depends on BUSD-AAA or BUSD-XYZ + err = keeper.PairMapper.AddTradingPair(ctx, dextypes.NewTradingPair(types.NativeTokenSymbol, "AAA-000M", 1e8)) + err = keeper.PairMapper.AddTradingPair(ctx, dextypes.NewTradingPair(types.NativeTokenSymbol, "XYZ-000M", 1e8)) + err = keeper.PairMapper.AddTradingPair(ctx, dextypes.NewTradingPair("AAA-000M", "XYZ-000M", 1e8)) + err = keeper.CanDelistTradingPair(ctx, "AAA-000M", "XYZ-000M") + require.Nil(t, err) +} + +func TestKeeper_CanDelistMiniTradingPair(t *testing.T) { + ctx, _, keeper := setup() + err := keeper.PairMapper.AddTradingPair(ctx, dextypes.NewTradingPair("AAA-000M", types.NativeTokenSymbol, 1e8)) + err = keeper.PairMapper.AddTradingPair(ctx, dextypes.NewTradingPair("AAA-000M", "BUSD-BD1", 1e8)) + err = keeper.CanDelistTradingPair(ctx, "AAA-000M", types.NativeTokenSymbol) + err = keeper.CanDelistTradingPair(ctx, "AAA-000M", "BUSD-BD1") + require.Nil(t, err) +} + +func setChainVersion() { + upgrade.Mgr.AddUpgradeHeight(upgrade.BEP8, -1) + upgrade.Mgr.AddUpgradeHeight(upgrade.BEP70, -1) +} + +func resetChainVersion() { + upgrade.Mgr.Config.HeightMap = nil +} diff --git a/plugins/dex/order/mini_keeper.go b/plugins/dex/order/mini_keeper.go new file mode 100644 index 000000000..60f9c3098 --- /dev/null +++ b/plugins/dex/order/mini_keeper.go @@ -0,0 +1,98 @@ +package order + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + bnclog "github.com/binance-chain/node/common/log" + "github.com/binance-chain/node/common/upgrade" + dexUtils "github.com/binance-chain/node/plugins/dex/utils" +) + +const ( + defaultMiniBlockMatchInterval int = 16 + defaultActiveMiniSymbolCount int = 8 +) + +//order keeper for mini-token +type MiniOrderKeeper struct { + BaseOrderKeeper + symbolSelector MiniSymbolSelector +} + +var _ DexOrderKeeper = &MiniOrderKeeper{} + +// NewBEP2OrderKeeper - Returns the MiniToken orderKeeper +func NewMiniOrderKeeper() DexOrderKeeper { + return &MiniOrderKeeper{ + BaseOrderKeeper: NewBaseOrderKeeper("dexMiniKeeper"), + symbolSelector: MiniSymbolSelector{ + make(map[string]uint32, 256), + make([]string, 0, 256), + }, + } +} + +//override +func (kp *MiniOrderKeeper) support(pair string) bool { + if !sdk.IsUpgrade(upgrade.BEP8) { + return false + } + return dexUtils.IsMiniTokenTradingPair(pair) +} + +//override +func (kp *MiniOrderKeeper) supportUpgradeVersion() bool { + return sdk.IsUpgrade(upgrade.BEP8) +} + +func (kp *MiniOrderKeeper) supportPairType(pairType SymbolPairType) bool { + return PairType.MINI == pairType +} + +// override +func (kp *MiniOrderKeeper) initOrders(symbol string) { + kp.allOrders[symbol] = map[string]*OrderInfo{} + kp.symbolSelector.addSymbolHash(symbol) +} + +func (kp *MiniOrderKeeper) clearAfterMatch() { + kp.logger.Debug("clearAfterMatchMini...") + for _, symbol := range kp.symbolSelector.roundSelectedSymbols { + delete(kp.roundOrders, symbol) + delete(kp.roundIOCOrders, symbol) + } + kp.symbolSelector.clearRoundMatchSymbol() +} + +func (kp *MiniOrderKeeper) iterateRoundSelectedPairs(iter func(string)) { + for _, symbol := range kp.symbolSelector.roundSelectedSymbols { + iter(symbol) + } +} + +func (kp *MiniOrderKeeper) getRoundPairsNum() int { + return len(kp.symbolSelector.roundSelectedSymbols) +} + +func (kp *MiniOrderKeeper) getRoundOrdersNum() int { + n := 0 + kp.iterateRoundSelectedPairs(func(symbol string) { + n += len(kp.roundOrders[symbol]) + }) + return n +} + +func (kp *MiniOrderKeeper) reloadOrder(symbol string, orderInfo *OrderInfo, height int64) { + kp.allOrders[symbol][orderInfo.Id] = orderInfo + //TODO confirm no round orders for mini symbol + if kp.collectOrderInfoForPublish { + if _, exists := kp.orderInfosForPub[orderInfo.Id]; !exists { + bnclog.Debug("add order to order changes map, during load snapshot, from active orders", "orderId", orderInfo.Id) + kp.orderInfosForPub[orderInfo.Id] = orderInfo + } + } +} + +func (kp *MiniOrderKeeper) selectSymbolsToMatch(height int64, matchAllSymbols bool) []string { + return kp.symbolSelector.SelectSymbolsToMatch(kp.roundOrders, height, matchAllSymbols) +} diff --git a/plugins/dex/order/openOrders_test.go b/plugins/dex/order/openOrders_test.go index 4865ab172..0c9c52444 100644 --- a/plugins/dex/order/openOrders_test.go +++ b/plugins/dex/order/openOrders_test.go @@ -20,7 +20,7 @@ var ( zc, _ = sdk.AccAddressFromBech32(ZcAddr) ) -func initKeeper() *Keeper { +func initKeeper() *DexKeeper { cdc := MakeCodec() keeper := MakeKeeper(cdc) return keeper diff --git a/plugins/dex/order/order_keeper.go b/plugins/dex/order/order_keeper.go new file mode 100644 index 000000000..02146f75c --- /dev/null +++ b/plugins/dex/order/order_keeper.go @@ -0,0 +1,289 @@ +package order + +import ( + "sync" + + sdk "github.com/cosmos/cosmos-sdk/types" + tmlog "github.com/tendermint/tendermint/libs/log" + + bnclog "github.com/binance-chain/node/common/log" + "github.com/binance-chain/node/common/utils" + me "github.com/binance-chain/node/plugins/dex/matcheng" + "github.com/binance-chain/node/plugins/dex/store" + dexUtils "github.com/binance-chain/node/plugins/dex/utils" +) + +const ( + numPricesStored = 2000 + pricesStoreEvery = 1000 + minimalNumPrices = 500 +) + +type DexOrderKeeper interface { + initOrders(symbol string) + addOrder(symbol string, info OrderInfo, isRecovery bool) + reloadOrder(symbol string, orderInfo *OrderInfo, height int64) + removeOrder(dexKeeper *DexKeeper, id string, symbol string) (ord me.OrderPart, err error) + orderExists(symbol, id string) (OrderInfo, bool) + getOpenOrders(pair string, addr sdk.AccAddress) []store.OpenOrder + getAllOrders() map[string]map[string]*OrderInfo + deleteOrdersForPair(pair string) + + iterateRoundSelectedPairs(func(string)) + iterateAllOrders(func(symbol string, id string)) + + getRoundOrdersNum() int + getAllOrdersForPair(pair string) map[string]*OrderInfo + getRoundOrdersForPair(pair string) []string + getRoundIOCOrdersForPair(pair string) []string + clearAfterMatch() + selectSymbolsToMatch(height int64, matchAllSymbols bool) []string + + // publish + enablePublish() + appendOrderChangeSync(change OrderChange) + getOrderChanges() OrderChanges + clearOrderChanges() + getOrderInfosForPub() OrderInfoForPublish + removeOrderInfosForPub(orderId string) + + support(pair string) bool + supportUpgradeVersion() bool + supportPairType(pairType SymbolPairType) bool +} + +// in the future, this may be distributed via Sharding +type BaseOrderKeeper struct { + allOrders map[string]map[string]*OrderInfo // symbol -> order ID -> order + roundOrders map[string][]string // limit to the total tx number in a block + roundIOCOrders map[string][]string + + collectOrderInfoForPublish bool + orderChangesMtx *sync.Mutex // guard orderChanges and orderInfosForPub during PreDevlierTx (which is async) + orderChanges OrderChanges // order changed in this block, will be cleaned before matching for new block + orderInfosForPub OrderInfoForPublish // for publication usage + + logger tmlog.Logger +} + +func NewBaseOrderKeeper(moduleName string) BaseOrderKeeper { + logger := bnclog.With("module", moduleName) + return BaseOrderKeeper{ + // need to init the nested map when a new symbol added. + allOrders: make(map[string]map[string]*OrderInfo, 256), + roundOrders: make(map[string][]string, 256), + roundIOCOrders: make(map[string][]string, 256), + + collectOrderInfoForPublish: false, // default to false, need a explicit set if needed + orderChangesMtx: &sync.Mutex{}, + orderChanges: make(OrderChanges, 0), + orderInfosForPub: make(OrderInfoForPublish), + logger: logger, + } +} + +func (kp *BaseOrderKeeper) addOrder(symbol string, info OrderInfo, isRecovery bool) { + if kp.collectOrderInfoForPublish { + change := OrderChange{info.Id, Ack, "", nil} + // deliberately not add this message to orderChanges + if !isRecovery { + kp.orderChanges = append(kp.orderChanges, change) + } + kp.logger.Debug("add order to order changes map", "orderId", info.Id, "isRecovery", isRecovery) + kp.orderInfosForPub[info.Id] = &info + } + + kp.allOrders[symbol][info.Id] = &info + kp.addRoundOrders(symbol, info) +} + +func (kp *BaseOrderKeeper) addRoundOrders(symbol string, info OrderInfo) { + if ids, ok := kp.roundOrders[symbol]; ok { + kp.roundOrders[symbol] = append(ids, info.Id) + } else { + newIds := make([]string, 0, 16) + kp.roundOrders[symbol] = append(newIds, info.Id) + } + if info.TimeInForce == TimeInForce.IOC { + kp.roundIOCOrders[symbol] = append(kp.roundIOCOrders[symbol], info.Id) + } +} + +func (kp *BaseOrderKeeper) orderExists(symbol, id string) (OrderInfo, bool) { + if orders, ok := kp.allOrders[symbol]; ok { + if msg, ok := orders[id]; ok { + return *msg, ok + } + } + return OrderInfo{}, false +} + +func (kp *BaseOrderKeeper) removeOrder(dexKeeper *DexKeeper, id string, symbol string) (ord me.OrderPart, err error) { + ordMsg, ok := kp.orderExists(symbol, id) + if !ok { + return me.OrderPart{}, orderNotFound(symbol, id) + } + eng, ok := dexKeeper.engines[symbol] + if !ok { + return me.OrderPart{}, orderNotFound(symbol, id) + } + delete(kp.allOrders[symbol], id) + return eng.Book.RemoveOrder(id, ordMsg.Side, ordMsg.Price) +} + +func (kp *BaseOrderKeeper) deleteOrdersForPair(pair string) { + delete(kp.allOrders, pair) +} + +func (kp *BaseOrderKeeper) getOpenOrders(pair string, addr sdk.AccAddress) []store.OpenOrder { + openOrders := make([]store.OpenOrder, 0) + + for _, order := range kp.allOrders[pair] { + if string(order.Sender.Bytes()) == string(addr.Bytes()) { + openOrders = append( + openOrders, + store.OpenOrder{ + order.Id, + pair, + utils.Fixed8(order.Price), + utils.Fixed8(order.Quantity), + utils.Fixed8(order.CumQty), + order.CreatedHeight, + order.CreatedTimestamp, + order.LastUpdatedHeight, + order.LastUpdatedTimestamp, + }) + } + } + + return openOrders +} + +func (kp *BaseOrderKeeper) getAllOrders() map[string]map[string]*OrderInfo { + return kp.allOrders +} + +func (kp *BaseOrderKeeper) clearOrderChanges() { + kp.orderChanges = kp.orderChanges[:0] +} + +func (kp *BaseOrderKeeper) enablePublish() { + kp.collectOrderInfoForPublish = true +} + +func (kp *BaseOrderKeeper) appendOrderChangeSync(change OrderChange) { + kp.orderChangesMtx.Lock() + kp.orderChanges = append(kp.orderChanges, change) + kp.orderChangesMtx.Unlock() +} + +func (kp *BaseOrderKeeper) getOrderChanges() OrderChanges { + return kp.orderChanges +} + +func (kp *BaseOrderKeeper) getOrderInfosForPub() OrderInfoForPublish { + return kp.orderInfosForPub +} + +func (kp *BaseOrderKeeper) removeOrderInfosForPub(orderId string) { + delete(kp.orderInfosForPub, orderId) +} + +func (kp *BaseOrderKeeper) getRoundOrdersForPair(pair string) []string { + return kp.roundOrders[pair] +} + +func (kp *BaseOrderKeeper) getRoundIOCOrdersForPair(pair string) []string { + return kp.roundIOCOrders[pair] +} + +func (kp *BaseOrderKeeper) getAllOrdersForPair(pair string) map[string]*OrderInfo { + return kp.allOrders[pair] +} + +func (kp *BaseOrderKeeper) iterateAllOrders(iter func(string, string)) { + for symbol, orders := range kp.allOrders { + for orderId := range orders { + iter(symbol, orderId) + } + } +} + +//------ BEP2OrderKeeper methods ----- +var _ DexOrderKeeper = &BEP2OrderKeeper{} + +type BEP2OrderKeeper struct { + BaseOrderKeeper + symbolSelector BEP2SymbolSelector +} + +// NewBEP2OrderKeeper - Returns the BEP2OrderKeeper +func NewBEP2OrderKeeper() DexOrderKeeper { + return &BEP2OrderKeeper{ + BaseOrderKeeper: NewBaseOrderKeeper("BEP2OrderKeeper"), + symbolSelector: BEP2SymbolSelector{}, + } +} + +func (kp *BEP2OrderKeeper) support(pair string) bool { + if !sdk.IsUpgrade(sdk.BEP8) { + return true + } + return !dexUtils.IsMiniTokenTradingPair(pair) +} + +func (kp *BEP2OrderKeeper) supportUpgradeVersion() bool { + return true +} + +func (kp *BEP2OrderKeeper) supportPairType(pairType SymbolPairType) bool { + return PairType.BEP2 == pairType +} + +func (kp *BEP2OrderKeeper) initOrders(symbol string) { + kp.allOrders[symbol] = map[string]*OrderInfo{} +} + +func (kp *BEP2OrderKeeper) clearAfterMatch() { + kp.logger.Debug("clearAfterMatchBEP2...") + kp.roundOrders = make(map[string][]string, 256) + kp.roundIOCOrders = make(map[string][]string, 256) +} + +func (kp *BEP2OrderKeeper) iterateRoundSelectedPairs(iter func(string)) { + for symbol := range kp.roundOrders { + iter(symbol) + } +} + +func (kp *BEP2OrderKeeper) reloadOrder(symbol string, orderInfo *OrderInfo, height int64) { + kp.allOrders[symbol][orderInfo.Id] = orderInfo + if orderInfo.CreatedHeight == height { + kp.roundOrders[symbol] = append(kp.roundOrders[symbol], orderInfo.Id) + if orderInfo.TimeInForce == TimeInForce.IOC { + kp.roundIOCOrders[symbol] = append(kp.roundIOCOrders[symbol], orderInfo.Id) + } + } + if kp.collectOrderInfoForPublish { + if _, exists := kp.orderInfosForPub[orderInfo.Id]; !exists { + bnclog.Debug("add order to order changes map, during load snapshot, from active orders", "orderId", orderInfo.Id) + kp.orderInfosForPub[orderInfo.Id] = orderInfo + } + } +} + +func (kp *BEP2OrderKeeper) getRoundPairsNum() int { + return len(kp.roundOrders) +} + +func (kp *BEP2OrderKeeper) getRoundOrdersNum() int { + n := 0 + for _, orders := range kp.roundOrders { + n += len(orders) + } + return n +} + +func (kp *BEP2OrderKeeper) selectSymbolsToMatch(height int64, matchAllSymbols bool) []string { + return kp.symbolSelector.SelectSymbolsToMatch(kp.roundOrders, height, matchAllSymbols) +} diff --git a/plugins/dex/order/quickselect.go b/plugins/dex/order/quickselect.go new file mode 100644 index 000000000..4ac89dca6 --- /dev/null +++ b/plugins/dex/order/quickselect.go @@ -0,0 +1,57 @@ +package order + +import "github.com/tendermint/tendermint/libs/common" + +//Find and return top K symbols with largest number of order. +// The returned top K slice is not sorted. The input orderNums may be re-ordered in place. +// If more than one symbols have same order numbers, these symbol will be selected by ascending alphabetical sequence. +func findTopKLargest(orderNums []*SymbolWithOrderNumber, k int) []*SymbolWithOrderNumber { + if k >= len(orderNums) { + return orderNums + } + return quickselect(orderNums, 0, len(orderNums)-1, k) +} + +func partition(orderNums []*SymbolWithOrderNumber, start, end int) int { + // move pivot to end + if end == start { + return start + } + + pivot := common.RandIntn(end-start) + start + orderNums[end], orderNums[pivot] = orderNums[pivot], orderNums[end] + pivotValue := orderNums[end] + i := start + for j := start; j < end; j++ { + if compare(orderNums[j], pivotValue) { + orderNums[i], orderNums[j] = orderNums[j], orderNums[i] + i++ + } + } + // move pivot to its sorted position + orderNums[i], orderNums[end] = orderNums[end], orderNums[i] + // return pivot index + return i +} + +func compare(orderNumA *SymbolWithOrderNumber, orderNumB *SymbolWithOrderNumber) bool { + if orderNumA.numberOfOrders > orderNumB.numberOfOrders { + return true + } else if orderNumA.numberOfOrders == orderNumB.numberOfOrders { + return orderNumA.symbol < orderNumB.symbol + } + return false +} + +func quickselect(orderNums []*SymbolWithOrderNumber, start, end, n int) []*SymbolWithOrderNumber { + // use last element as pivot + pivotIndex := partition(orderNums, start, end) + + if n-1 == pivotIndex { + return orderNums[:pivotIndex+1] + } else if n-1 > pivotIndex { + return quickselect(orderNums, pivotIndex+1, end, n) + } else { + return quickselect(orderNums, start, pivotIndex-1, n) + } +} diff --git a/plugins/dex/order/quickselect_test.go b/plugins/dex/order/quickselect_test.go new file mode 100644 index 000000000..c2f136dd3 --- /dev/null +++ b/plugins/dex/order/quickselect_test.go @@ -0,0 +1,119 @@ +package order + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/libs/common" +) + +func Test_findKthLargest(t *testing.T) { + //A0 := &SymbolWithOrderNumber{"ABC-9UEM_BNB", 0} + A1 := &SymbolWithOrderNumber{"ABC-9UEM_BNB", 1} + A2 := &SymbolWithOrderNumber{"ABC-9UEM_BNB", 2} + A3 := &SymbolWithOrderNumber{"ABC-9UEM_BNB", 3} + A4 := &SymbolWithOrderNumber{"ABC-9UEM_BNB", 4} + A5 := &SymbolWithOrderNumber{"ABC-9UEM_BNB", 5} + A6 := &SymbolWithOrderNumber{"ABC-9UEM_BNB", 6} + //B2 := &SymbolWithOrderNumber{"b", 2} + B3 := &SymbolWithOrderNumber{"BAC-678M_BNB", 3} + B5 := &SymbolWithOrderNumber{"BAC-678M_BNB", 5} + //C3 := &SymbolWithOrderNumber{"c", 3} + C4 := &SymbolWithOrderNumber{"CUY-G42M_BNB", 4} + //D4 := &SymbolWithOrderNumber{"d", 4} + D3 := &SymbolWithOrderNumber{"DUY-765_BNB", 3} + //E5 := &SymbolWithOrderNumber{"e", 5} + E2 := &SymbolWithOrderNumber{"ETF-876_BNB", 2} + //F6 := &SymbolWithOrderNumber{"f", 6} + F1 := &SymbolWithOrderNumber{"FXM-987M_BNB", 1} + + expected := []*SymbolWithOrderNumber{A3, A4, A5} + result := findTopKLargest([]*SymbolWithOrderNumber{A1, A2, A3, A4, A5}, 3) + assertResult(t, expected, result) + + expected = []*SymbolWithOrderNumber{A3, A4, A5} + result = findTopKLargest([]*SymbolWithOrderNumber{A5, A3, A3, A1, A4}, 3) + assertResult(t, expected, result) + + expected = []*SymbolWithOrderNumber{A3, A4, A5} + result = findTopKLargest([]*SymbolWithOrderNumber{A5, B3, A3, A1, A4}, 3) + assertResult(t, expected, result) + + expected = []*SymbolWithOrderNumber{A6, A5} + result = findTopKLargest([]*SymbolWithOrderNumber{A3, A2, A1, A5, A6, A4}, 2) + assertResult(t, expected, result) + + expected = []*SymbolWithOrderNumber{A6, C4, B5} + result = findTopKLargest([]*SymbolWithOrderNumber{D3, E2, F1, B5, A6, C4}, 3) + assertResult(t, expected, result) + + expected = []*SymbolWithOrderNumber{D3, E2, F1, B5, A6, C4} + result = findTopKLargest([]*SymbolWithOrderNumber{D3, E2, F1, B5, A6, C4}, 6) + assertResult(t, expected, result) + + expected = []*SymbolWithOrderNumber{D3, E2, F1, B5, A6, C4} + result = findTopKLargest([]*SymbolWithOrderNumber{D3, E2, F1, B5, A6, C4}, 7) + assertResult(t, expected, result) +} + +func Test_findKthLargest_SameNumber(t *testing.T) { + A0 := &SymbolWithOrderNumber{"ABC-9UEM_BNB", 0} + A1 := &SymbolWithOrderNumber{"ABC-9UEM_BNB", 1} + A2 := &SymbolWithOrderNumber{"ABC-9UEM_BNB", 2} + B2 := &SymbolWithOrderNumber{"BAC-678M_BNB", 2} + C1 := &SymbolWithOrderNumber{"CUY-G42M_BNB", 1} + C2 := &SymbolWithOrderNumber{"CUY-G42M_BNB", 2} + E2 := &SymbolWithOrderNumber{"ETF-876_BNB", 2} + F2 := &SymbolWithOrderNumber{"FXM-987M_BNB", 2} + + assert := assert.New(t) + + expected := []*SymbolWithOrderNumber{A2, B2, E2} + result := findTopKLargest([]*SymbolWithOrderNumber{F2, E2, A2, B2, C1}, 3) + assertResult(t, expected, result) + + expected = []*SymbolWithOrderNumber{B2, A2, C2} + result = findTopKLargest([]*SymbolWithOrderNumber{A0, A1, A2, B2, C1, C2}, 3) + assertResult(t, expected, result) + + expected = []*SymbolWithOrderNumber{A2} + result = findTopKLargest([]*SymbolWithOrderNumber{A0, A1, A2, B2, C1, C2}, 1) + assertResult(t, expected, result) + + expected = []*SymbolWithOrderNumber{A1, A2, B2, C2} + result = findTopKLargest([]*SymbolWithOrderNumber{A0, A1, A2, B2, C1, C2}, 4) + assert.Equal(4, len(result)) + assertResult(t, expected, result) + + expected = []*SymbolWithOrderNumber{A1, A2, B2, C1, C2} + result = findTopKLargest([]*SymbolWithOrderNumber{A0, A1, A2, B2, C1, C2}, 5) + assert.Equal(5, len(result)) + assertResult(t, expected, result) +} + +func Benchmark_findTopKLargest(b *testing.B) { + const size = 10000 + origin := make([]*SymbolWithOrderNumber, size) + for i := 0; i < b.N; i++ { + b.StopTimer() + for i := 0; i < size; i++ { + origin[i] = &SymbolWithOrderNumber{symbol: string(common.RandBytes(10)), numberOfOrders: int(common.RandIntn(size / 10))} + } + b.StartTimer() + findTopKLargest(origin, size/2) + } +} + +func assertResult(t *testing.T, expected []*SymbolWithOrderNumber, actual []*SymbolWithOrderNumber) { + var s string + for _, x := range actual { + s += fmt.Sprintf("%v,", *x) + } + t.Logf(s) + require.Equal(t, len(expected), len(actual)) + for _, ele := range expected { + require.Contains(t, actual, ele, "Expected contains %v, but doesn't exist", ele) + } +} diff --git a/plugins/dex/order/symbol_selector.go b/plugins/dex/order/symbol_selector.go new file mode 100644 index 000000000..91ae115de --- /dev/null +++ b/plugins/dex/order/symbol_selector.go @@ -0,0 +1,90 @@ +package order + +import ( + "hash/crc32" +) + +type SymbolSelector interface { + SelectSymbolsToMatch(roundOrders map[string][]string, height int64, matchAllSymbols bool) []string +} + +var _ SymbolSelector = &BEP2SymbolSelector{} + +type BEP2SymbolSelector struct{} + +func (bss *BEP2SymbolSelector) SelectSymbolsToMatch(roundOrders map[string][]string, height int64, matchAllSymbols bool) []string { + size := len(roundOrders) + if size == 0 { + return make([]string, 0) + } + symbolsToMatch := make([]string, 0, len(roundOrders)) + for symbol := range roundOrders { + symbolsToMatch = append(symbolsToMatch, symbol) + } + return symbolsToMatch +} + +type MiniSymbolSelector struct { + symbolsHash map[string]uint32 //mini token pairs -> hash value for Round-Robin + roundSelectedSymbols []string //mini token pairs to match in this round +} + +var _ SymbolSelector = &MiniSymbolSelector{} + +func (mss *MiniSymbolSelector) addSymbolHash(symbol string) { + mss.symbolsHash[symbol] = crc32.ChecksumIEEE([]byte(symbol)) +} + +func (mss *MiniSymbolSelector) clearRoundMatchSymbol() { + mss.roundSelectedSymbols = make([]string, 0) +} + +func (mss *MiniSymbolSelector) SelectSymbolsToMatch(roundOrders map[string][]string, height int64, matchAllSymbols bool) []string { + size := len(roundOrders) + if size == 0 { + return make([]string, 0) + } + symbolsToMatch := make([]string, 0, len(roundOrders)) + if matchAllSymbols { + for symbol := range roundOrders { + symbolsToMatch = append(symbolsToMatch, symbol) + } + } else { + mss.selectMiniSymbolsToMatch(roundOrders, height, func(miniSymbols map[string]struct{}) { + for symbol := range miniSymbols { + symbolsToMatch = append(symbolsToMatch, symbol) + } + }) + } + mss.roundSelectedSymbols = symbolsToMatch + return symbolsToMatch +} + +func (mss *MiniSymbolSelector) selectMiniSymbolsToMatch(roundOrders map[string][]string, height int64, postSelect func(map[string]struct{})) { + symbolsToMatch := make(map[string]struct{}, 256) + mss.selectActiveMiniSymbols(symbolsToMatch, roundOrders, defaultActiveMiniSymbolCount) + mss.selectMiniSymbolsRoundRobin(symbolsToMatch, height, defaultMiniBlockMatchInterval) + postSelect(symbolsToMatch) +} + +func (mss *MiniSymbolSelector) selectActiveMiniSymbols(symbolsToMatch map[string]struct{}, roundOrdersMini map[string][]string, k int) { + //use quick select to select top k symbols + symbolOrderNumsSlice := make([]*SymbolWithOrderNumber, 0, len(roundOrdersMini)) + for symbol, orders := range roundOrdersMini { + symbolOrderNumsSlice = append(symbolOrderNumsSlice, &SymbolWithOrderNumber{symbol, len(orders)}) + } + topKSymbolOrderNums := findTopKLargest(symbolOrderNumsSlice, k) + + for _, selected := range topKSymbolOrderNums { + symbolsToMatch[selected.symbol] = struct{}{} + } +} + +func (mss *MiniSymbolSelector) selectMiniSymbolsRoundRobin(symbolsToMatch map[string]struct{}, height int64, matchInterval int) { + m := height % int64(matchInterval) + for symbol, symbolHash := range mss.symbolsHash { + if int64(symbolHash%uint32(matchInterval)) == m { + symbolsToMatch[symbol] = struct{}{} + } + } +} diff --git a/plugins/dex/order/transfer.go b/plugins/dex/order/transfer.go index 9b116c996..60692759d 100644 --- a/plugins/dex/order/transfer.go +++ b/plugins/dex/order/transfer.go @@ -167,6 +167,7 @@ func transferFromOrderRemoved(ord me.OrderPart, ordMsg OrderInfo, tranEventType outAsset: unlockAsset, out: unlock, unlock: unlock, + Symbol: ordMsg.Symbol, } } diff --git a/plugins/dex/order/types.go b/plugins/dex/order/types.go index a2317a633..d3cbefbff 100644 --- a/plugins/dex/order/types.go +++ b/plugins/dex/order/types.go @@ -121,6 +121,12 @@ type ExpireHolder struct { OrderId string Reason ChangeType Fee string + Symbol string +} + +type SymbolWithOrderNumber struct { + symbol string + numberOfOrders int } type FeeHolder map[string]*types.Fee diff --git a/plugins/dex/plugin.go b/plugins/dex/plugin.go index 33e8c1238..42a6d161a 100644 --- a/plugins/dex/plugin.go +++ b/plugins/dex/plugin.go @@ -6,37 +6,39 @@ import ( "time" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/gov" "github.com/binance-chain/node/app/pub" bnclog "github.com/binance-chain/node/common/log" app "github.com/binance-chain/node/common/types" "github.com/binance-chain/node/plugins/dex/utils" - tkstore "github.com/binance-chain/node/plugins/tokens/store" + "github.com/binance-chain/node/plugins/tokens" ) -const AbciQueryPrefix = "dex" +const DexAbciQueryPrefix = "dex" +const DexMiniAbciQueryPrefix = "dex-mini" const DelayedDaysForDelist = 3 // InitPlugin initializes the dex plugin. func InitPlugin( - appp app.ChainApp, keeper *DexKeeper, tokenMapper tkstore.Mapper, accMapper auth.AccountKeeper, govKeeper gov.Keeper, + appp app.ChainApp, dexKeeper *DexKeeper, tokenMapper tokens.Mapper, govKeeper gov.Keeper, ) { - cdc := appp.GetCodec() // add msg handlers - for route, handler := range Routes(cdc, keeper, tokenMapper, accMapper, govKeeper) { + for route, handler := range Routes(dexKeeper, tokenMapper, govKeeper) { appp.GetRouter().AddRoute(route, handler) } // add abci handlers - handler := createQueryHandler(keeper) - appp.RegisterQueryHandler(AbciQueryPrefix, handler) + dexHandler := createQueryHandler(dexKeeper, DexAbciQueryPrefix) + appp.RegisterQueryHandler(DexAbciQueryPrefix, dexHandler) + //dex mini handler + dexMiniHandler := createQueryHandler(dexKeeper, DexMiniAbciQueryPrefix) + appp.RegisterQueryHandler(DexMiniAbciQueryPrefix, dexMiniHandler) } -func createQueryHandler(keeper *DexKeeper) app.AbciQueryHandler { - return createAbciQueryHandler(keeper) +func createQueryHandler(keeper *DexKeeper, abciQueryPrefix string) app.AbciQueryHandler { + return createAbciQueryHandler(keeper, abciQueryPrefix) } // EndBreatheBlock processes the breathe block lifecycle event. @@ -50,7 +52,7 @@ func EndBreatheBlock(ctx sdk.Context, dexKeeper *DexKeeper, govKeeper gov.Keeper dexKeeper.UpdateTickSizeAndLotSize(ctx) logger.Info("Expire stale orders") - if dexKeeper.CollectOrderInfoForPublish { + if dexKeeper.ShouldPublishOrder() { pub.ExpireOrdersForPublish(dexKeeper, ctx, blockTime) } else { dexKeeper.ExpireOrders(ctx, blockTime, nil) @@ -78,7 +80,7 @@ func delistTradingPairs(ctx sdk.Context, govKeeper gov.Keeper, dexKeeper *DexKee continue } - if dexKeeper.CollectOrderInfoForPublish { + if dexKeeper.ShouldPublishOrder() { pub.DelistTradingPairForPublish(ctx, dexKeeper, symbol) } else { dexKeeper.DelistTradingPair(ctx, symbol, nil) @@ -115,7 +117,6 @@ func getSymbolsToDelist(ctx sdk.Context, govKeeper gov.Keeper, blockTime time.Ti if timeToDelist.Before(blockTime) { symbol := utils.Assets2TradingPair(strings.ToUpper(delistParam.BaseAssetSymbol), strings.ToUpper(delistParam.QuoteAssetSymbol)) symbols = append(symbols, symbol) - // update proposal delisted status delistParam.IsExecuted = true bz, err := json.Marshal(delistParam) diff --git a/plugins/dex/route.go b/plugins/dex/route.go index 6153bb452..2b9eeb9df 100644 --- a/plugins/dex/route.go +++ b/plugins/dex/route.go @@ -2,20 +2,17 @@ package dex import ( sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/gov" "github.com/binance-chain/node/plugins/dex/list" "github.com/binance-chain/node/plugins/dex/order" "github.com/binance-chain/node/plugins/tokens" - "github.com/binance-chain/node/wire" ) // Routes exports dex message routes -func Routes(cdc *wire.Codec, dexKeeper *DexKeeper, tokenMapper tokens.Mapper, - accKeeper auth.AccountKeeper, govKeeper gov.Keeper) map[string]sdk.Handler { +func Routes(dexKeeper *DexKeeper, tokenMapper tokens.Mapper, govKeeper gov.Keeper) map[string]sdk.Handler { routes := make(map[string]sdk.Handler) - orderHandler := order.NewHandler(cdc, dexKeeper, accKeeper) + orderHandler := order.NewHandler(dexKeeper) routes[order.RouteNewOrder] = orderHandler routes[order.RouteCancelOrder] = orderHandler routes[list.Route] = list.NewHandler(dexKeeper, tokenMapper, govKeeper) diff --git a/plugins/dex/store/codec.go b/plugins/dex/store/codec.go index 301cf7aca..364cdf7c8 100644 --- a/plugins/dex/store/codec.go +++ b/plugins/dex/store/codec.go @@ -10,7 +10,9 @@ import ( // queryOrderBook queries the store for the serialized order book for a given pair. func queryOrderBook(cdc *wire.Codec, ctx context.CLIContext, pair string, levels int) (*[]byte, error) { - bz, err := ctx.Query(fmt.Sprintf("dex/orderbook/%s/%d", pair, levels), nil) + var path string + path = fmt.Sprintf("dex/orderbook/%s/%d", pair, levels) + bz, err := ctx.Query(path, nil) if err != nil { return nil, err } @@ -41,7 +43,9 @@ func GetOrderBook(cdc *wire.Codec, ctx context.CLIContext, pair string, levels i } func queryOpenOrders(cdc *wire.Codec, ctx context.CLIContext, pair string, addr string) (*[]byte, error) { - if bz, err := ctx.Query(fmt.Sprintf("dex/openorders/%s/%s", pair, addr), nil); err != nil { + var path string + path = fmt.Sprintf("dex/openorders/%s/%s", pair, addr) + if bz, err := ctx.Query(path, nil); err != nil { return nil, err } else { return &bz, nil diff --git a/plugins/dex/store/mapper.go b/plugins/dex/store/mapper.go index 2d799cc1e..5919f0a4a 100644 --- a/plugins/dex/store/mapper.go +++ b/plugins/dex/store/mapper.go @@ -44,12 +44,16 @@ func NewTradingPairMapper(cdc *wire.Codec, key sdk.StoreKey) TradingPairMapper { func (m mapper) AddTradingPair(ctx sdk.Context, pair types.TradingPair) error { baseAsset := pair.BaseAssetSymbol - if err := cmn.ValidateMapperTokenSymbol(baseAsset); err != nil { - return err - } quoteAsset := pair.QuoteAssetSymbol - if err := cmn.ValidateMapperTokenSymbol(quoteAsset); err != nil { - return err + if !cmn.IsValidMiniTokenSymbol(baseAsset) { + if err := cmn.ValidateTokenSymbol(baseAsset); err != nil { + return err + } + } + if !cmn.IsValidMiniTokenSymbol(quoteAsset) { + if err := cmn.ValidateTokenSymbol(quoteAsset); err != nil { + return err + } } tradeSymbol := dexUtils.Assets2TradingPair(strings.ToUpper(baseAsset), strings.ToUpper(quoteAsset)) diff --git a/plugins/dex/store/utils.go b/plugins/dex/store/utils.go index 221b258ca..3f39a594b 100644 --- a/plugins/dex/store/utils.go +++ b/plugins/dex/store/utils.go @@ -12,20 +12,16 @@ func ValidatePairSymbol(symbol string) error { if len(symbol) == 0 { return errors.New("symbol pair must not be empty") } - // TokenSymbolMaxLen: BTC00000 - // TokenSymbolTxHashSuffixLen: 000 - // + 2: ".B" - // * 2: BTC00000.B, ETH00000.B - // + 3: 2x `-` and 1x `_` - if len(symbol) > ((types.TokenSymbolMaxLen+types.TokenSymbolTxHashSuffixLen+2)*2)+3 { - return errors.New("symbol pair is too long") - } + tokenSymbols := strings.SplitN(strings.ToUpper(symbol), "_", 2) if len(tokenSymbols) != 2 { return errors.New("invalid symbol: trading pair must contain an underscore ('_')") } for _, tokenSymbol := range tokenSymbols { - if err := types.ValidateMapperTokenSymbol(tokenSymbol); err != nil { + if types.IsValidMiniTokenSymbol(tokenSymbol) { + continue + } + if err := types.ValidateTokenSymbol(tokenSymbol); err != nil { return err } } diff --git a/plugins/dex/utils/pair.go b/plugins/dex/utils/pair.go index ec106ce48..c28525748 100644 --- a/plugins/dex/utils/pair.go +++ b/plugins/dex/utils/pair.go @@ -6,6 +6,7 @@ import ( "math/big" "strings" + "github.com/binance-chain/node/common/types" "github.com/binance-chain/node/common/utils" ) @@ -83,3 +84,11 @@ func TradingPair2AssetsSafe(symbol string) (baseAsset, quoteAsset string) { func Assets2TradingPair(baseAsset, quoteAsset string) (symbol string) { return fmt.Sprintf("%s_%s", baseAsset, quoteAsset) } + +func IsMiniTokenTradingPair(symbol string) bool { + baseAsset, quoteAsset, err := TradingPair2Assets(symbol) + if err != nil { + return false + } + return types.IsMiniTokenSymbol(baseAsset) || types.IsMiniTokenSymbol(quoteAsset) +} diff --git a/plugins/dex/wire.go b/plugins/dex/wire.go index cf5c3f784..e0900b5e0 100644 --- a/plugins/dex/wire.go +++ b/plugins/dex/wire.go @@ -18,6 +18,8 @@ func RegisterWire(cdc *wire.Codec) { cdc.RegisterConcrete(list.ListMsg{}, "dex/ListMsg", nil) cdc.RegisterConcrete(types.TradingPair{}, "dex/TradingPair", nil) + cdc.RegisterConcrete(list.ListMiniMsg{}, "dex/ListMiniMsg", nil) + cdc.RegisterConcrete(order.FeeConfig{}, "dex/FeeConfig", nil) cdc.RegisterConcrete(order.OrderBookSnapshot{}, "dex/OrderBookSnapshot", nil) cdc.RegisterConcrete(order.ActiveOrders{}, "dex/ActiveOrders", nil) diff --git a/plugins/param/genesis.go b/plugins/param/genesis.go index c8cde3b0c..27f10ce16 100644 --- a/plugins/param/genesis.go +++ b/plugins/param/genesis.go @@ -52,6 +52,12 @@ const ( FeeRateNative = 400 IOCExpireFee = 25e3 IOCExpireFeeNative = 5e3 + + //MiniToken fee + TinyIssueFee = 2e8 + MiniIssueFee = 4e8 + MiniSetUriFee = 37500 + MiniListingFee = 10e8 ) var DefaultGenesisState = param.GenesisState{ diff --git a/plugins/param/plugin.go b/plugins/param/plugin.go index bc6e369d3..101973ed3 100644 --- a/plugins/param/plugin.go +++ b/plugins/param/plugin.go @@ -19,6 +19,7 @@ import ( "github.com/binance-chain/node/plugins/tokens/burn" "github.com/binance-chain/node/plugins/tokens/freeze" "github.com/binance-chain/node/plugins/tokens/issue" + miniURI "github.com/binance-chain/node/plugins/tokens/seturi" "github.com/binance-chain/node/plugins/tokens/swap" "github.com/binance-chain/node/plugins/tokens/timelock" ) @@ -60,6 +61,15 @@ func RegisterUpgradeBeginBlocker(paramHub *ParamHub) { } paramHub.UpdateFeeParams(ctx, swapFeeParams) }) + upgrade.Mgr.RegisterBeginBlocker(upgrade.BEP8, func(ctx sdk.Context) { + miniTokenFeeParams := []param.FeeParam{ + ¶m.FixedFeeParams{MsgType: issue.IssueTinyMsgType, Fee: TinyIssueFee, FeeFor: types.FeeForAll}, + ¶m.FixedFeeParams{MsgType: issue.IssueMiniMsgType, Fee: MiniIssueFee, FeeFor: types.FeeForAll}, + ¶m.FixedFeeParams{MsgType: miniURI.SetURIMsg{}.Type(), Fee: MiniSetUriFee, FeeFor: types.FeeForProposer}, + ¶m.FixedFeeParams{MsgType: list.ListMiniMsg{}.Type(), Fee: MiniListingFee, FeeFor: types.FeeForAll}, + } + paramHub.UpdateFeeParams(ctx, miniTokenFeeParams) + }) } func EndBreatheBlock(ctx sdk.Context, paramHub *ParamHub) { @@ -92,5 +102,9 @@ func init() { swap.DepositHTLT: fees.FixedFeeCalculatorGen, swap.ClaimHTLT: fees.FixedFeeCalculatorGen, swap.RefundHTLT: fees.FixedFeeCalculatorGen, + issue.IssueTinyMsgType: fees.FixedFeeCalculatorGen, + issue.IssueMiniMsgType: fees.FixedFeeCalculatorGen, + miniURI.SetURIRoute: fees.FixedFeeCalculatorGen, + list.ListMiniMsg{}.Type(): fees.FixedFeeCalculatorGen, } } diff --git a/plugins/param/types/types.go b/plugins/param/types/types.go index ce39679f3..4acab6873 100644 --- a/plugins/param/types/types.go +++ b/plugins/param/types/types.go @@ -40,6 +40,11 @@ var ( "depositHTLT": {}, "claimHTLT": {}, "refundHTLT": {}, + + "tinyIssueMsg": {}, + "miniIssueMsg": {}, + "miniTokensSetURI": {}, + "dexListMini": {}, } ValidTransferFeeMsgTypes = map[string]struct{}{ diff --git a/plugins/tokens/abci.go b/plugins/tokens/abci.go index 3b8b7a470..e850195ad 100644 --- a/plugins/tokens/abci.go +++ b/plugins/tokens/abci.go @@ -9,13 +9,23 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" - app "github.com/binance-chain/node/common/types" + "github.com/binance-chain/node/common/types" ) -func createAbciQueryHandler(mapper Mapper) app.AbciQueryHandler { - return func(app app.ChainApp, req abci.RequestQuery, path []string) (res *abci.ResponseQuery) { +func createAbciQueryHandler(mapper Mapper, prefix string) types.AbciQueryHandler { + queryPrefix := prefix + var isMini bool + switch queryPrefix { + case abciQueryPrefix: + isMini = false + case miniAbciQueryPrefix: + isMini = true + default: + isMini = false + } + return func(app types.ChainApp, req abci.RequestQuery, path []string) (res *abci.ResponseQuery) { // expects at least two query path segments. - if path[0] != abciQueryPrefix || len(path) < 2 { + if path[0] != queryPrefix || len(path) < 2 { return nil } switch path[1] { @@ -25,7 +35,7 @@ func createAbciQueryHandler(mapper Mapper) app.AbciQueryHandler { Code: uint32(sdk.CodeUnknownRequest), Log: fmt.Sprintf( "%s %s query requires a symbol path arg", - abciQueryPrefix, path[1]), + queryPrefix, path[1]), } } ctx := app.GetContextForCheckState() @@ -36,31 +46,14 @@ func createAbciQueryHandler(mapper Mapper) app.AbciQueryHandler { Log: "empty symbol not permitted", } } - token, err := mapper.GetToken(ctx, symbol) - if err != nil { - return &abci.ResponseQuery{ - Code: uint32(sdk.CodeInternal), - Log: err.Error(), - } - } - bz, err := app.GetCodec().MarshalBinaryLengthPrefixed(token) - if err != nil { - return &abci.ResponseQuery{ - Code: uint32(sdk.CodeInternal), - Log: err.Error(), - } - } - return &abci.ResponseQuery{ - Code: uint32(sdk.ABCICodeOK), - Value: bz, - } + return queryAndMarshallToken(app, mapper, ctx, symbol) case "list": // args: ["tokens", "list", , , ] if len(path) < 4 { return &abci.ResponseQuery{ Code: uint32(sdk.CodeUnknownRequest), Log: fmt.Sprintf( "%s %s query requires offset and limit path segments", - abciQueryPrefix, path[1]), + queryPrefix, path[1]), } } showZeroSupplyTokens := false @@ -68,7 +61,9 @@ func createAbciQueryHandler(mapper Mapper) app.AbciQueryHandler { showZeroSupplyTokens = true } ctx := app.GetContextForCheckState() - tokens := mapper.GetTokenList(ctx, showZeroSupplyTokens) + + tokens := mapper.GetTokenList(ctx, showZeroSupplyTokens, isMini) + offset, err := strconv.Atoi(path[2]) if err != nil || offset < 0 || offset >= len(tokens) { return &abci.ResponseQuery{ @@ -93,9 +88,24 @@ func createAbciQueryHandler(mapper Mapper) app.AbciQueryHandler { Log: "malformed range", } } - bz, err := app.GetCodec().MarshalBinaryLengthPrefixed( - tokens[offset:end], - ) + var bz []byte + if isMini { + miniTokens := make([]*types.MiniToken, end-offset) + for i, token := range tokens[offset:end] { + miniTokens[i] = token.(*types.MiniToken) + } + bz, err = app.GetCodec().MarshalBinaryLengthPrefixed( + miniTokens, + ) + } else { + bep2Tokens := make([]*types.Token, end-offset) + for i, token := range tokens[offset:end] { + bep2Tokens[i] = token.(*types.Token) + } + bz, err = app.GetCodec().MarshalBinaryLengthPrefixed( + bep2Tokens, + ) + } if err != nil { return &abci.ResponseQuery{ Code: uint32(sdk.CodeInternal), @@ -111,8 +121,33 @@ func createAbciQueryHandler(mapper Mapper) app.AbciQueryHandler { Code: uint32(sdk.ABCICodeOK), Info: fmt.Sprintf( "Unknown `%s` query path: %v", - abciQueryPrefix, path), + queryPrefix, path), } } } } + +func queryAndMarshallToken(app types.ChainApp, mapper Mapper, ctx sdk.Context, symbol string) *abci.ResponseQuery { + var bz []byte + var err error + var token types.IToken + + token, err = mapper.GetToken(ctx, symbol) + if err != nil { + return &abci.ResponseQuery{ + Code: uint32(sdk.CodeInternal), + Log: err.Error(), + } + } + bz, err = app.GetCodec().MarshalBinaryLengthPrefixed(token) + if err != nil { + return &abci.ResponseQuery{ + Code: uint32(sdk.CodeInternal), + Log: err.Error(), + } + } + return &abci.ResponseQuery{ + Code: uint32(sdk.ABCICodeOK), + Value: bz, + } +} diff --git a/plugins/tokens/abci_test.go b/plugins/tokens/abci_test.go index 542ab4b22..e949c03cd 100644 --- a/plugins/tokens/abci_test.go +++ b/plugins/tokens/abci_test.go @@ -26,8 +26,8 @@ var ( addr = sdk.AccAddress(pk.Address()) token1Ptr, _ = common.NewToken("XXX", "XXX-000", 10000000000, addr, false) token2Ptr, _ = common.NewToken("XXY", "XXY-000", 10000000000, addr, false) - token1 = *token1Ptr - token2 = *token2Ptr + token1 = token1Ptr + token2 = token2Ptr ) func Test_Tokens_ABCI_GetInfo_Success(t *testing.T) { @@ -53,7 +53,7 @@ func Test_Tokens_ABCI_GetInfo_Success(t *testing.T) { } assert.True(t, sdk.ABCICodeType(res.Code).IsOK()) - assert.Equal(t, token1, actual) + assert.Equal(t, *token1, actual) } func Test_Tokens_ABCI_GetInfo_Error_NotFound(t *testing.T) { @@ -115,7 +115,7 @@ func Test_Tokens_ABCI_GetTokens_Success(t *testing.T) { assert.True(t, sdk.ABCICodeType(res.Code).IsOK()) assert.Equal(t, []common.Token{ - token1, token2, + *token1, *token2, }, actual) } @@ -147,7 +147,7 @@ func Test_Tokens_ABCI_GetTokens_Success_WithOffset(t *testing.T) { assert.True(t, sdk.ABCICodeType(res.Code).IsOK()) assert.Equal(t, []common.Token{ - token2, + *token2, }, actual) } @@ -179,7 +179,7 @@ func Test_Tokens_ABCI_GetTokens_Success_WithLimit(t *testing.T) { assert.True(t, sdk.ABCICodeType(res.Code).IsOK()) assert.Equal(t, []common.Token{ - token1, + *token1, }, actual) } diff --git a/plugins/tokens/burn/handler.go b/plugins/tokens/burn/handler.go index 2bd715183..fe05a1905 100644 --- a/plugins/tokens/burn/handler.go +++ b/plugins/tokens/burn/handler.go @@ -1,6 +1,7 @@ package burn import ( + "fmt" "reflect" "strings" @@ -8,6 +9,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/bank" "github.com/binance-chain/node/common/log" + common "github.com/binance-chain/node/common/types" "github.com/binance-chain/node/plugins/tokens/store" ) @@ -16,7 +18,6 @@ func NewHandler(tokenMapper store.Mapper, keeper bank.Keeper) sdk.Handler { if msg, ok := msg.(BurnMsg); ok { return handleBurnToken(ctx, tokenMapper, keeper, msg) } - errMsg := "Unrecognized msg type: " + reflect.TypeOf(msg).Name() return sdk.ErrUnknownRequest(errMsg).Result() } @@ -33,18 +34,27 @@ func handleBurnToken(ctx sdk.Context, tokenMapper store.Mapper, keeper bank.Keep } if !token.IsOwner(msg.From) { - logger.Info("burn token failed", "reason", "not token's owner", "from", msg.From, "owner", token.Owner) + logger.Info("burn token failed", "reason", "not token's owner", "from", msg.From, "owner", token.GetOwner()) return sdk.ErrUnauthorized("only the owner of the token can burn the token").Result() } - coins := keeper.GetCoins(ctx, token.Owner) - if coins.AmountOf(symbol) < burnAmount || - token.TotalSupply.ToInt64() < burnAmount { + coins := keeper.GetCoins(ctx, token.GetOwner()) + balance := coins.AmountOf(symbol) + if balance < burnAmount || + token.GetTotalSupply().ToInt64() < burnAmount { logger.Info("burn token failed", "reason", "no enough tokens to burn") return sdk.ErrInsufficientCoins("do not have enough token to burn").Result() } - _, _, sdkError := keeper.SubtractCoins(ctx, token.Owner, sdk.Coins{{ + if common.IsMiniTokenSymbol(symbol) { + if burnAmount < common.MiniTokenMinExecutionAmount && balance != burnAmount { + logger.Info("burn token failed", "reason", "burn amount doesn't reach the min amount") + return sdk.ErrInvalidCoins(fmt.Sprintf("burn amount is too small, the min amount is %d or total free balance", + common.MiniTokenMinExecutionAmount)).Result() + } + } + + _, _, sdkError := keeper.SubtractCoins(ctx, token.GetOwner(), sdk.Coins{{ Denom: symbol, Amount: burnAmount, }}) @@ -53,7 +63,7 @@ func handleBurnToken(ctx sdk.Context, tokenMapper store.Mapper, keeper bank.Keep return sdkError.Result() } - newTotalSupply := token.TotalSupply.ToInt64() - burnAmount + newTotalSupply := token.GetTotalSupply().ToInt64() - burnAmount err = tokenMapper.UpdateTotalSupply(ctx, symbol, newTotalSupply) if err != nil { logger.Error("burn token failed", "reason", "update total supply failed: "+err.Error()) diff --git a/plugins/tokens/burn/handler_test.go b/plugins/tokens/burn/handler_test.go new file mode 100644 index 000000000..f38447ea6 --- /dev/null +++ b/plugins/tokens/burn/handler_test.go @@ -0,0 +1,159 @@ +package burn + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/bank" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + + "github.com/binance-chain/node/common/testutils" + "github.com/binance-chain/node/common/types" + "github.com/binance-chain/node/common/upgrade" + "github.com/binance-chain/node/plugins/tokens/issue" + "github.com/binance-chain/node/plugins/tokens/store" + "github.com/binance-chain/node/wire" +) + +func setup() (sdk.Context, sdk.Handler, sdk.Handler, auth.AccountKeeper, store.Mapper) { + ms, capKey1, capKey2 := testutils.SetupMultiStoreForUnitTest() + cdc := wire.NewCodec() + cdc.RegisterInterface((*types.IToken)(nil), nil) + cdc.RegisterConcrete(&types.Token{}, "bnbchain/Token", nil) + cdc.RegisterConcrete(&types.MiniToken{}, "bnbchain/MiniToken", nil) + tokenMapper := store.NewMapper(cdc, capKey1) + accountKeeper := auth.NewAccountKeeper(cdc, capKey2, types.ProtoAppAccount) + bankKeeper := bank.NewBaseKeeper(accountKeeper) + handler := NewHandler(tokenMapper, bankKeeper) + tokenHandler := issue.NewHandler(tokenMapper, bankKeeper) + + accountStore := ms.GetKVStore(capKey2) + accountStoreCache := auth.NewAccountStoreCache(cdc, accountStore, 10) + ctx := sdk.NewContext(ms, abci.Header{ChainID: "mychainid", Height: 1}, + sdk.RunTxModeDeliver, log.NewNopLogger()). + WithAccountCache(auth.NewAccountCache(accountStoreCache)) + return ctx, handler, tokenHandler, accountKeeper, tokenMapper +} + +func setChainVersion() { + upgrade.Mgr.AddUpgradeHeight(upgrade.BEP8, -1) +} + +func resetChainVersion() { + upgrade.Mgr.Config.HeightMap = nil +} + +func TestHandleBurnMini(t *testing.T) { + setChainVersion() + defer resetChainVersion() + ctx, handler, miniIssueHandler, accountKeeper, tokenMapper := setup() + _, acc := testutils.NewAccount(ctx, accountKeeper, 100e8) + + ctx = ctx.WithValue(baseapp.TxHashKey, "000") + msg := issue.NewIssueMiniMsg(acc.GetAddress(), "New BNB", "NNB", 10000e8, false, "http://www.xyz.com/nnb.json") + sdkResult := miniIssueHandler(ctx, msg) + require.Equal(t, true, sdkResult.Code.IsOK()) + + token, err := tokenMapper.GetToken(ctx, "NNB-000M") + require.NoError(t, err) + expectedToken := types.NewMiniToken("New BNB", "NNB", "NNB-000M", 2, 10000e8, acc.GetAddress(), false, "http://www.xyz.com/nnb.json") + require.Equal(t, expectedToken, token) + + ctx = ctx.WithValue(baseapp.TxHashKey, "002") + burnMsg := NewMsg(acc.GetAddress(), "NNB-000M", 10001e8+1) + sdkResult = handler(ctx, burnMsg) + require.Equal(t, false, sdkResult.Code.IsOK()) + require.Contains(t, sdkResult.Log, "do not have enough token to burn") + + ctx = ctx.WithValue(baseapp.TxHashKey, "002") + burnMsg = NewMsg(acc.GetAddress(), "NNB-000M", 9999e8+1) + sdkResult = handler(ctx, burnMsg) + require.Equal(t, true, sdkResult.Code.IsOK()) + + token, err = tokenMapper.GetToken(ctx, "NNB-000M") + require.NoError(t, err) + expectedToken = types.NewMiniToken("New BNB", "NNB", "NNB-000M", 2, 1e8-1, acc.GetAddress(), false, "http://www.xyz.com/nnb.json") + require.Equal(t, expectedToken, token) + + account := accountKeeper.GetAccount(ctx, msg.From).(types.NamedAccount) + amount := account.GetCoins().AmountOf("NNB-000M") + require.Equal(t, int64(1e8-1), amount) + + ctx = ctx.WithValue(baseapp.TxHashKey, "002") + burnMsg = NewMsg(acc.GetAddress(), "NNB-000M", 1e8-2) + sdkResult = handler(ctx, burnMsg) + require.Equal(t, false, sdkResult.Code.IsOK()) + + ctx = ctx.WithValue(baseapp.TxHashKey, "002") + burnMsg = NewMsg(acc.GetAddress(), "NNB-000M", 1e8-1) + sdkResult = handler(ctx, burnMsg) + require.Equal(t, true, sdkResult.Code.IsOK()) + + token, err = tokenMapper.GetToken(ctx, "NNB-000M") + require.NoError(t, err) + expectedToken = types.NewMiniToken("New BNB", "NNB", "NNB-000M", 2, 0, acc.GetAddress(), false, "http://www.xyz.com/nnb.json") + require.Equal(t, expectedToken, token) + + _, acc2 := testutils.NewAccount(ctx, accountKeeper, 100e8) + ctx = ctx.WithValue(baseapp.TxHashKey, "002") + burnMsg = NewMsg(acc2.GetAddress(), "NNB-000M", 1e8) + sdkResult = handler(ctx, burnMsg) + require.Equal(t, false, sdkResult.Code.IsOK()) + require.Contains(t, sdkResult.Log, "only the owner of the token can burn the token") + + account = accountKeeper.GetAccount(ctx, msg.From).(types.NamedAccount) + amount = account.GetCoins().AmountOf("NNB-000M") + require.Equal(t, int64(0), amount) +} + +func TestHandleBurn(t *testing.T) { + setChainVersion() + defer resetChainVersion() + ctx, handler, issueHandler, accountKeeper, tokenMapper := setup() + _, acc := testutils.NewAccount(ctx, accountKeeper, 100e8) + + ctx = ctx.WithValue(baseapp.TxHashKey, "000") + msg := issue.NewIssueMsg(acc.GetAddress(), "New BNB", "NNB", 10000e8, false) + sdkResult := issueHandler(ctx, msg) + require.Equal(t, true, sdkResult.Code.IsOK()) + + _, err := tokenMapper.GetToken(ctx, "NNB-000M") + require.NotNil(t, err) + require.Contains(t, err.Error(), "token(NNB-000M) not found") + + token, err := tokenMapper.GetToken(ctx, "NNB-000") + require.Equal(t, true, sdkResult.Code.IsOK()) + + ctx = ctx.WithValue(baseapp.TxHashKey, "002") + burnMsg := NewMsg(acc.GetAddress(), "NNB-000", 10001e8) + sdkResult = handler(ctx, burnMsg) + require.Equal(t, false, sdkResult.Code.IsOK()) + require.Contains(t, sdkResult.Log, "do not have enough token to burn") + + ctx = ctx.WithValue(baseapp.TxHashKey, "002") + burnMsg = NewMsg(acc.GetAddress(), "NNB-000", 9999e8+1) + sdkResult = handler(ctx, burnMsg) + require.Equal(t, true, sdkResult.Code.IsOK()) + + token, err = tokenMapper.GetToken(ctx, "NNB-000") + require.NoError(t, err) + expectedToken, err := types.NewToken("New BNB", "NNB-000", 1e8-1, acc.GetAddress(), false) + require.Equal(t, expectedToken, token) + + account := accountKeeper.GetAccount(ctx, msg.From).(types.NamedAccount) + amount := account.GetCoins().AmountOf("NNB-000") + require.Equal(t, int64(1e8-1), amount) + + _, acc2 := testutils.NewAccount(ctx, accountKeeper, 100e8) + ctx = ctx.WithValue(baseapp.TxHashKey, "002") + burnMsg = NewMsg(acc2.GetAddress(), "NNB-000", 1e8) + sdkResult = handler(ctx, burnMsg) + require.Equal(t, false, sdkResult.Code.IsOK()) + require.Contains(t, sdkResult.Log, "only the owner of the token can burn the token") +} diff --git a/plugins/tokens/burn/msg.go b/plugins/tokens/burn/msg.go index 176d12851..884ca6266 100644 --- a/plugins/tokens/burn/msg.go +++ b/plugins/tokens/burn/msg.go @@ -7,6 +7,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/binance-chain/node/common/types" + "github.com/binance-chain/node/common/upgrade" ) // TODO: "route expressions can only contain alphanumeric characters", we need to change the cosmos sdk to support slash @@ -40,8 +41,15 @@ func (msg BurnMsg) GetSigners() []sdk.AccAddress { return []sdk.AccAdd // ValidateBasic does a simple validation check that // doesn't require access to any other information. func (msg BurnMsg) ValidateBasic() sdk.Error { + if sdk.IsUpgrade(upgrade.BEP8) && types.IsValidMiniTokenSymbol(msg.Symbol) { + if msg.Amount <= 0 { + return sdk.ErrInsufficientFunds("amount should be more than 0") + } + return nil + } + // if BEP8 not upgraded, we rely on `ValidateTokenSymbol` rejecting the MiniToken. // expect all msgs that reference a token after issue to use the suffixed form (e.g. "BNB-ABC") - err := types.ValidateMapperTokenSymbol(msg.Symbol) + err := types.ValidateTokenSymbol(msg.Symbol) if err != nil { return sdk.ErrInvalidCoins(err.Error()) } diff --git a/plugins/tokens/client/cli/commands.go b/plugins/tokens/client/cli/commands.go index f0c6cc093..67de8c4f6 100644 --- a/plugins/tokens/client/cli/commands.go +++ b/plugins/tokens/client/cli/commands.go @@ -51,7 +51,15 @@ func AddCommands(cmd *cobra.Command, cdc *wire.Codec) { client.PostCommands(MultiSendCmd(cdc))..., ) + tokenCmd.AddCommand( + client.PostCommands( + issueMiniTokenCmd(cmdr), + issueTinyTokenCmd(cmdr), + setTokenURICmd(cmdr))..., + ) + tokenCmd.AddCommand(client.LineBreak) cmd.AddCommand(tokenCmd) + } diff --git a/plugins/tokens/client/cli/helper.go b/plugins/tokens/client/cli/helper.go index dc33e121e..07fd494f3 100644 --- a/plugins/tokens/client/cli/helper.go +++ b/plugins/tokens/client/cli/helper.go @@ -1,6 +1,7 @@ package commands import ( + "bytes" "errors" "strconv" "strings" @@ -15,6 +16,8 @@ import ( "github.com/binance-chain/node/wire" ) +const miniTokenKeyPrefix = "mini" + type Commander struct { Cdc *wire.Codec } @@ -30,9 +33,11 @@ func (c Commander) checkAndSendTx(cmd *cobra.Command, args []string, builder msg } symbol := viper.GetString(flagSymbol) - err = types.ValidateMapperTokenSymbol(symbol) - if err != nil { - return err + if !types.IsValidMiniTokenSymbol(symbol) { + err = types.ValidateTokenSymbol(symbol) + if err != nil { + return err + } } symbol = strings.ToUpper(symbol) @@ -60,3 +65,18 @@ func parseAmount(amountStr string) (int64, error) { return amount, nil } + +func validateTokenURI(uri string) error { + if len(uri) > 2048 { + return errors.New("uri cannot be longer than 2048 characters") + } + return nil +} + +func calcMiniTokenKey(symbol string) []byte { + var buf bytes.Buffer + buf.WriteString(miniTokenKeyPrefix) + buf.WriteString(":") + buf.WriteString(symbol) + return buf.Bytes() +} diff --git a/plugins/tokens/client/cli/info.go b/plugins/tokens/client/cli/info.go index 625ab4395..b662dea5c 100644 --- a/plugins/tokens/client/cli/info.go +++ b/plugins/tokens/client/cli/info.go @@ -18,7 +18,7 @@ import ( func getTokenInfoCmd(cmdr Commander) *cobra.Command { cmd := &cobra.Command{ Use: "info ", - Short: "Query token info", + Short: "Query token/mini-token info", RunE: cmdr.runGetToken, } @@ -34,7 +34,12 @@ func (c Commander) runGetToken(cmd *cobra.Command, args []string) error { return errors.New("you must provide the symbol") } - key := []byte(strings.ToUpper(symbol)) + var key []byte + if types.IsMiniTokenSymbol(symbol) { + key = calcMiniTokenKey(strings.ToUpper(symbol)) + } else { + key = []byte(strings.ToUpper(symbol)) + } res, err := ctx.QueryStore(key, common.TokenStoreName) if err != nil { @@ -47,14 +52,14 @@ func (c Commander) runGetToken(cmd *cobra.Command, args []string) error { } // decode the value - token := new(types.Token) + var token types.IToken err = c.Cdc.UnmarshalBinaryBare(res, &token) if err != nil { return err } // print out the toke info - output, err := wire.MarshalJSONIndent(c.Cdc, token) + output, err := wire.MarshalJSONIndent(c.Cdc, &token) if err != nil { return err } diff --git a/plugins/tokens/client/cli/issue.go b/plugins/tokens/client/cli/issue.go index a60c6f72f..222e51a8b 100644 --- a/plugins/tokens/client/cli/issue.go +++ b/plugins/tokens/client/cli/issue.go @@ -1,6 +1,8 @@ package commands import ( + "strings" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -57,7 +59,7 @@ func (c Commander) issueToken(cmd *cobra.Command, args []string) error { } symbol := viper.GetString(flagSymbol) - err = types.ValidateIssueMsgTokenSymbol(symbol) + err = types.ValidateIssueSymbol(symbol) if err != nil { return err } @@ -83,15 +85,22 @@ func (c Commander) mintToken(cmd *cobra.Command, args []string) error { } symbol := viper.GetString(flagSymbol) - err = types.ValidateMapperTokenSymbol(symbol) - if err != nil { - return err - } - amount := viper.GetInt64(flagAmount) - err = checkSupplyAmount(amount) - if err != nil { - return err + + if types.IsValidMiniTokenSymbol(strings.ToUpper(symbol)) { + err = checkMiniTokenSupplyAmount(amount) + if err != nil { + return err + } + } else { + err = types.ValidateTokenSymbol(symbol) + if err != nil { + return err + } + err = checkSupplyAmount(amount) + if err != nil { + return err + } } msg := issue.NewMintMsg(from, symbol, amount) @@ -104,3 +113,10 @@ func checkSupplyAmount(amount int64) error { } return nil } +func checkMiniTokenSupplyAmount(amount int64) error { + if amount <= types.MiniTokenMinExecutionAmount || amount > types.MiniTokenSupplyUpperBound { + return errors.New("invalid supply amount") + } + + return nil +} diff --git a/plugins/tokens/client/cli/issue_mini.go b/plugins/tokens/client/cli/issue_mini.go new file mode 100644 index 000000000..80190062c --- /dev/null +++ b/plugins/tokens/client/cli/issue_mini.go @@ -0,0 +1,80 @@ +package commands + +import ( + "fmt" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/binance-chain/node/common/client" + "github.com/binance-chain/node/common/types" + "github.com/binance-chain/node/plugins/tokens/issue" +) + +const ( + flagTokenUri = "token-uri" +) + +func issueMiniTokenCmd(cmdr Commander) *cobra.Command { + cmd := &cobra.Command{ + Use: "issue-mini", + Short: "issue a new mini-token", + RunE: cmdr.issueMiniToken, + } + + cmd.Flags().String(flagTokenName, "", "name of the new token") + cmd.Flags().StringP(flagSymbol, "s", "", "symbol of the new token") + cmd.Flags().Int64P(flagTotalSupply, "n", 0, "total supply of the new token") + cmd.Flags().Bool(flagMintable, false, "whether the token can be minted") + cmd.Flags().String(flagTokenUri, "", "uri of the token information") + cmd.MarkFlagRequired(flagTotalSupply) + return cmd +} + +func (c Commander) issueMiniToken(cmd *cobra.Command, args []string) error { + cliCtx, txBldr := client.PrepareCtx(c.Cdc) + from, err := cliCtx.GetFromAddress() + if err != nil { + return err + } + + name := viper.GetString(flagTokenName) + if len(name) == 0 { + return errors.New("you must provide the name of the token") + } + + symbol := viper.GetString(flagSymbol) + err = types.ValidateIssueMiniSymbol(symbol) + if err != nil { + return err + } + + supply := viper.GetInt64(flagTotalSupply) + err = checkMiniSupplyAmount(supply, int8(types.MiniRangeType)) + if err != nil { + return err + } + + mintable := viper.GetBool(flagMintable) + + tokenURI := viper.GetString(flagTokenUri) + err = validateTokenURI(tokenURI) + if err != nil { + return err + } + + // build message + msg := issue.NewIssueMiniMsg(from, name, symbol, supply, mintable, tokenURI) + return client.SendOrPrintTx(cliCtx, txBldr, msg) +} + +func checkMiniSupplyAmount(amount int64, tokenType int8) error { + if amount <= types.MiniTokenMinExecutionAmount || amount > types.MiniTokenSupplyUpperBound { + return errors.New("invalid supply amount") + } + if amount > types.SupplyRangeType(tokenType).UpperBound() { + return errors.New(fmt.Sprintf("supply amount cannot exceed max supply amount of %s - %d", types.SupplyRangeType(tokenType).String(), types.SupplyRangeType(tokenType).UpperBound())) + } + return nil +} diff --git a/plugins/tokens/client/cli/issue_tiny.go b/plugins/tokens/client/cli/issue_tiny.go new file mode 100644 index 000000000..b0fb3a9e5 --- /dev/null +++ b/plugins/tokens/client/cli/issue_tiny.go @@ -0,0 +1,64 @@ +package commands + +import ( + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/binance-chain/node/common/client" + "github.com/binance-chain/node/common/types" + "github.com/binance-chain/node/plugins/tokens/issue" +) + +func issueTinyTokenCmd(cmdr Commander) *cobra.Command { + cmd := &cobra.Command{ + Use: "issue-tiny", + Short: "issue a new tiny-token", + RunE: cmdr.issueTinyToken, + } + + cmd.Flags().String(flagTokenName, "", "name of the new token") + cmd.Flags().StringP(flagSymbol, "s", "", "symbol of the new token") + cmd.Flags().Int64P(flagTotalSupply, "n", 0, "total supply of the new token") + cmd.Flags().Bool(flagMintable, false, "whether the token can be minted") + cmd.Flags().String(flagTokenUri, "", "uri of the token information") + cmd.MarkFlagRequired(flagTotalSupply) + return cmd +} + +func (c Commander) issueTinyToken(cmd *cobra.Command, args []string) error { + cliCtx, txBldr := client.PrepareCtx(c.Cdc) + from, err := cliCtx.GetFromAddress() + if err != nil { + return err + } + + name := viper.GetString(flagTokenName) + if len(name) == 0 { + return errors.New("you must provide the name of the token") + } + + symbol := viper.GetString(flagSymbol) + err = types.ValidateIssueMiniSymbol(symbol) + if err != nil { + return err + } + + supply := viper.GetInt64(flagTotalSupply) + err = checkMiniSupplyAmount(supply, int8(types.TinyRangeType)) + if err != nil { + return err + } + + mintable := viper.GetBool(flagMintable) + + tokenURI := viper.GetString(flagTokenUri) + err = validateTokenURI(tokenURI) + if err != nil { + return err + } + + // build message + msg := issue.NewIssueTinyMsg(from, name, symbol, supply, mintable, tokenURI) + return client.SendOrPrintTx(cliCtx, txBldr, msg) +} diff --git a/plugins/tokens/client/cli/seturi_mini.go b/plugins/tokens/client/cli/seturi_mini.go new file mode 100644 index 000000000..b92bb496c --- /dev/null +++ b/plugins/tokens/client/cli/seturi_mini.go @@ -0,0 +1,43 @@ +package commands + +import ( + "github.com/binance-chain/node/common/client" + "github.com/binance-chain/node/common/types" + "github.com/binance-chain/node/plugins/tokens/seturi" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func setTokenURICmd(cmdr Commander) *cobra.Command { + cmd := &cobra.Command{ + Use: "set-uri-mini --symbol {symbol} --uri {token uri} --from {token issuer address}", + Short: "set token URI of mini-token", + RunE: cmdr.setTokenURI, + } + + cmd.Flags().StringP(flagSymbol, "s", "", "symbol of the mini-token") + cmd.Flags().String(flagTokenUri, "", "uri of the token information") + + return cmd +} + +func (c Commander) setTokenURI(cmd *cobra.Command, args []string) error { + cliCtx, txBldr := client.PrepareCtx(c.Cdc) + from, err := cliCtx.GetFromAddress() + if err != nil { + return err + } + symbol := viper.GetString(flagSymbol) + err = types.ValidateMiniTokenSymbol(symbol) + if err != nil { + return err + } + tokenURI := viper.GetString(flagTokenUri) + err = validateTokenURI(tokenURI) + if err != nil { + return err + } + + msg := seturi.NewSetUriMsg(from, symbol, tokenURI) + return client.SendOrPrintTx(cliCtx, txBldr, msg) +} diff --git a/plugins/tokens/client/rest/gettoken.go b/plugins/tokens/client/rest/gettoken.go index 38378e9a9..19f7443c6 100644 --- a/plugins/tokens/client/rest/gettoken.go +++ b/plugins/tokens/client/rest/gettoken.go @@ -14,18 +14,29 @@ import ( "github.com/binance-chain/node/wire" ) -func getTokenInfo(ctx context.CLIContext, cdc *wire.Codec, symbol string) (*types.Token, error) { - bz, err := ctx.Query(fmt.Sprintf("tokens/info/%s", symbol), nil) +func getTokenInfo(ctx context.CLIContext, cdc *wire.Codec, symbol string, isMini bool) (types.IToken, error) { + var abciPrefix string + if isMini { + abciPrefix = "mini-tokens" + } else { + abciPrefix = "tokens" + } + bz, err := ctx.Query(fmt.Sprintf("%s/info/%s", abciPrefix, symbol), nil) + if err != nil { + return nil, err + } + + var token types.IToken + err = cdc.UnmarshalBinaryLengthPrefixed(bz, token) if err != nil { + fmt.Println(err) return nil, err } - var token types.Token - err = cdc.UnmarshalBinaryLengthPrefixed(bz, &token) - return &token, nil + return token, nil } // GetTokenReqHandler creates an http request handler to get info for an individual token -func GetTokenReqHandler(cdc *wire.Codec, ctx context.CLIContext) http.HandlerFunc { +func GetTokenReqHandler(cdc *wire.Codec, ctx context.CLIContext, isMini bool) http.HandlerFunc { type params struct { symbol string } @@ -55,7 +66,7 @@ func GetTokenReqHandler(cdc *wire.Codec, ctx context.CLIContext) http.HandlerFun return } - token, err := getTokenInfo(ctx, cdc, params.symbol) + token, err := getTokenInfo(ctx, cdc, params.symbol, isMini) if err != nil { throw(w, http.StatusInternalServerError, err) return diff --git a/plugins/tokens/client/rest/gettokens.go b/plugins/tokens/client/rest/gettokens.go index 51af8d5f0..93253f436 100644 --- a/plugins/tokens/client/rest/gettokens.go +++ b/plugins/tokens/client/rest/gettokens.go @@ -17,18 +17,28 @@ const maxTokensLimit = 1000 const defaultTokensLimit = 100 const defaultTokensOffset = 0 -func listAllTokens(ctx context.CLIContext, cdc *wire.Codec, offset int, limit int, showZeroSupplyTokens bool) ([]types.Token, error) { - bz, err := ctx.Query(fmt.Sprintf("tokens/list/%d/%d/%s", offset, limit, strconv.FormatBool(showZeroSupplyTokens)), nil) +func listAllTokens(ctx context.CLIContext, cdc *wire.Codec, offset int, limit int, showZeroSupplyTokens bool, isMini bool) (interface{}, error) { + var abciPrefix string + if isMini { + abciPrefix = "mini-tokens" + } else { + abciPrefix = "tokens" + } + bz, err := ctx.Query(fmt.Sprintf("%s/list/%d/%d/%s", abciPrefix, offset, limit, strconv.FormatBool(showZeroSupplyTokens)), nil) if err != nil { return nil, err } - tokens := make([]types.Token, 0) + + tokens := make([]types.IToken, 0) err = cdc.UnmarshalBinaryLengthPrefixed(bz, &tokens) + if err != nil { + return nil, err + } return tokens, nil } // GetTokensReqHandler creates an http request handler to get the list of tokens in the token mapper -func GetTokensReqHandler(cdc *wire.Codec, ctx context.CLIContext) http.HandlerFunc { +func GetTokensReqHandler(cdc *wire.Codec, ctx context.CLIContext, isMini bool) http.HandlerFunc { type params struct { limit int offset int @@ -88,7 +98,7 @@ func GetTokensReqHandler(cdc *wire.Codec, ctx context.CLIContext) http.HandlerFu params.limit = maxTokensLimit } - tokens, err := listAllTokens(ctx, cdc, params.offset, params.limit, params.showZeroSupplyTokens) + tokens, err := listAllTokens(ctx, cdc, params.offset, params.limit, params.showZeroSupplyTokens, isMini) if err != nil { throw(w, http.StatusInternalServerError, err) return diff --git a/plugins/tokens/freeze/handler.go b/plugins/tokens/freeze/handler.go index 50b41971e..89bb0be0b 100644 --- a/plugins/tokens/freeze/handler.go +++ b/plugins/tokens/freeze/handler.go @@ -1,6 +1,7 @@ package freeze import ( + "fmt" "reflect" "strings" @@ -33,11 +34,20 @@ func handleFreezeToken(ctx sdk.Context, tokenMapper store.Mapper, accKeeper auth symbol := strings.ToUpper(msg.Symbol) logger := log.With("module", "token", "symbol", symbol, "amount", freezeAmount, "addr", msg.From) coins := keeper.GetCoins(ctx, msg.From) - if coins.AmountOf(symbol) < freezeAmount { + balance := coins.AmountOf(symbol) + if balance < freezeAmount { logger.Info("freeze token failed", "reason", "no enough free tokens to freeze") return sdk.ErrInsufficientCoins("do not have enough token to freeze").Result() } + if common.IsMiniTokenSymbol(symbol) { + if msg.Amount < common.MiniTokenMinExecutionAmount && balance != freezeAmount { + logger.Info("freeze token failed", "reason", "freeze amount doesn't reach the min amount") + return sdk.ErrInvalidCoins(fmt.Sprintf("freeze amount is too small, the min amount is %d or total account balance", + common.MiniTokenMinExecutionAmount)).Result() + } + } + account := accKeeper.GetAccount(ctx, msg.From).(common.NamedAccount) newFrozenTokens := account.GetFrozenCoins().Plus(sdk.Coins{{Denom: symbol, Amount: freezeAmount}}) newFreeTokens := account.GetCoins().Minus(sdk.Coins{{Denom: symbol, Amount: freezeAmount}}) @@ -52,6 +62,7 @@ func handleUnfreezeToken(ctx sdk.Context, tokenMapper store.Mapper, accKeeper au unfreezeAmount := msg.Amount symbol := strings.ToUpper(msg.Symbol) logger := log.With("module", "token", "symbol", symbol, "amount", unfreezeAmount, "addr", msg.From) + account := accKeeper.GetAccount(ctx, msg.From).(common.NamedAccount) frozenAmount := account.GetFrozenCoins().AmountOf(symbol) if frozenAmount < unfreezeAmount { @@ -59,6 +70,14 @@ func handleUnfreezeToken(ctx sdk.Context, tokenMapper store.Mapper, accKeeper au return sdk.ErrInsufficientCoins("do not have enough token to unfreeze").Result() } + if common.IsMiniTokenSymbol(symbol) { + if unfreezeAmount < common.MiniTokenMinExecutionAmount && frozenAmount != unfreezeAmount { + logger.Info("unfreeze token failed", "reason", "unfreeze amount doesn't reach the min amount") + return sdk.ErrInvalidCoins(fmt.Sprintf("freeze amount is too small, the min amount is %d or total frozen balance", + common.MiniTokenMinExecutionAmount)).Result() + } + } + newFrozenTokens := account.GetFrozenCoins().Minus(sdk.Coins{{Denom: symbol, Amount: unfreezeAmount}}) newFreeTokens := account.GetCoins().Plus(sdk.Coins{{Denom: symbol, Amount: unfreezeAmount}}) account.SetFrozenCoins(newFrozenTokens) diff --git a/plugins/tokens/freeze/handler_test.go b/plugins/tokens/freeze/handler_test.go new file mode 100644 index 000000000..eac894eff --- /dev/null +++ b/plugins/tokens/freeze/handler_test.go @@ -0,0 +1,209 @@ +package freeze + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/bank" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + + "github.com/binance-chain/node/common/testutils" + "github.com/binance-chain/node/common/types" + "github.com/binance-chain/node/common/upgrade" + "github.com/binance-chain/node/plugins/tokens/issue" + "github.com/binance-chain/node/plugins/tokens/store" + "github.com/binance-chain/node/wire" +) + +func setup() (sdk.Context, sdk.Handler, sdk.Handler, auth.AccountKeeper, store.Mapper) { + ms, capKey1, capKey2 := testutils.SetupMultiStoreForUnitTest() + cdc := wire.NewCodec() + cdc.RegisterInterface((*types.IToken)(nil), nil) + cdc.RegisterConcrete(&types.Token{}, "bnbchain/Token", nil) + cdc.RegisterConcrete(&types.MiniToken{}, "bnbchain/MiniToken", nil) + tokenMapper := store.NewMapper(cdc, capKey1) + //app.AccountKeeper = auth.NewAccountKeeper(cdc, common.AccountStoreKey, types.ProtoAppAccount) + accountKeeper := auth.NewAccountKeeper(cdc, capKey2, types.ProtoAppAccount) + bankKeeper := bank.NewBaseKeeper(accountKeeper) + handler := NewHandler(tokenMapper, accountKeeper, bankKeeper) + tokenHandler := issue.NewHandler(tokenMapper, bankKeeper) + + accountStore := ms.GetKVStore(capKey2) + accountStoreCache := auth.NewAccountStoreCache(cdc, accountStore, 10) + ctx := sdk.NewContext(ms, abci.Header{ChainID: "mychainid", Height: 1}, + sdk.RunTxModeDeliver, log.NewNopLogger()). + WithAccountCache(auth.NewAccountCache(accountStoreCache)) + return ctx, handler, tokenHandler, accountKeeper, tokenMapper +} + +func setChainVersion() { + upgrade.Mgr.AddUpgradeHeight(upgrade.BEP8, -1) +} + +func resetChainVersion() { + upgrade.Mgr.Config.HeightMap = nil +} + +func TestHandleFreezeMini(t *testing.T) { + setChainVersion() + defer resetChainVersion() + ctx, handler, miniIssueHandler, accountKeeper, tokenMapper := setup() + _, acc := testutils.NewAccount(ctx, accountKeeper, 100e8) + + ctx = ctx.WithValue(baseapp.TxHashKey, "000") + msg := issue.NewIssueMiniMsg(acc.GetAddress(), "New BNB", "NNB", 10000e8, false, "http://www.xyz.com/nnb.json") + sdkResult := miniIssueHandler(ctx, msg) + require.Equal(t, true, sdkResult.Code.IsOK()) + + token, err := tokenMapper.GetToken(ctx, "NNB-000M") + require.NoError(t, err) + expectedToken := types.NewMiniToken("New BNB", "NNB", "NNB-000M", 2, 10000e8, acc.GetAddress(), false, "http://www.xyz.com/nnb.json") + require.Equal(t, expectedToken, token) + + account := accountKeeper.GetAccount(ctx, msg.From).(types.NamedAccount) + amount := account.GetCoins().AmountOf("NNB-000M") + frozenAmount := account.GetFrozenCoins().AmountOf("NNB-000M") + require.Equal(t, int64(10000e8), amount) + require.Equal(t, int64(0), frozenAmount) + + ctx = ctx.WithValue(baseapp.TxHashKey, "002") + freezeMsg := NewFreezeMsg(acc.GetAddress(), "NNB-000M", 10000e8+1) + sdkResult = handler(ctx, freezeMsg) + require.Equal(t, false, sdkResult.Code.IsOK()) + require.Contains(t, sdkResult.Log, "do not have enough token to freeze") + + ctx = ctx.WithValue(baseapp.TxHashKey, "002") + freezeMsg = NewFreezeMsg(acc.GetAddress(), "NNB-000M", 1e8-1) + sdkResult = handler(ctx, freezeMsg) + require.Equal(t, false, sdkResult.Code.IsOK()) + require.Contains(t, sdkResult.Log, "freeze amount is too small") + + ctx = ctx.WithValue(baseapp.TxHashKey, "002") + freezeMsg = NewFreezeMsg(acc.GetAddress(), "NNB-000M", 9999e8+1) + sdkResult = handler(ctx, freezeMsg) + require.Equal(t, true, sdkResult.Code.IsOK()) + + account = accountKeeper.GetAccount(ctx, msg.From).(types.NamedAccount) + amount = account.GetCoins().AmountOf("NNB-000M") + frozenAmount = account.GetFrozenCoins().AmountOf("NNB-000M") + require.Equal(t, int64(1e8-1), amount) + require.Equal(t, int64(9999e8+1), frozenAmount) + + ctx = ctx.WithValue(baseapp.TxHashKey, "002") + freezeMsg = NewFreezeMsg(acc.GetAddress(), "NNB-000M", 1e8-1) + sdkResult = handler(ctx, freezeMsg) + require.Equal(t, true, sdkResult.Code.IsOK()) + + account = accountKeeper.GetAccount(ctx, msg.From).(types.NamedAccount) + amount = account.GetCoins().AmountOf("NNB-000M") + frozenAmount = account.GetFrozenCoins().AmountOf("NNB-000M") + require.Equal(t, int64(0), amount) + require.Equal(t, int64(10000e8), frozenAmount) + + token, err = tokenMapper.GetToken(ctx, "NNB-000M") + require.NoError(t, err) + expectedToken = types.NewMiniToken("New BNB", "NNB", "NNB-000M", 2, 10000e8, acc.GetAddress(), false, "http://www.xyz.com/nnb.json") + require.Equal(t, expectedToken, token) + + ctx = ctx.WithValue(baseapp.TxHashKey, "003") + unfreezeMsg := NewUnfreezeMsg(acc.GetAddress(), "NNB-000M", 1e8-1) + sdkResult = handler(ctx, unfreezeMsg) + require.Equal(t, false, sdkResult.Code.IsOK()) + require.Contains(t, sdkResult.Log, "freeze amount is too small") + + unfreezeMsg = NewUnfreezeMsg(acc.GetAddress(), "NNB-000M", 1e8) + sdkResult = handler(ctx, unfreezeMsg) + require.Equal(t, true, sdkResult.Code.IsOK()) + account = accountKeeper.GetAccount(ctx, msg.From).(types.NamedAccount) + amount = account.GetCoins().AmountOf("NNB-000M") + frozenAmount = account.GetFrozenCoins().AmountOf("NNB-000M") + require.Equal(t, int64(1e8), amount) + require.Equal(t, int64(9999e8), frozenAmount) + + unfreezeMsg = NewUnfreezeMsg(acc.GetAddress(), "NNB-000M", 9999e8-1) + sdkResult = handler(ctx, unfreezeMsg) + require.Equal(t, true, sdkResult.Code.IsOK()) + account = accountKeeper.GetAccount(ctx, msg.From).(types.NamedAccount) + amount = account.GetCoins().AmountOf("NNB-000M") + frozenAmount = account.GetFrozenCoins().AmountOf("NNB-000M") + require.Equal(t, int64(10000e8-1), amount) + require.Equal(t, int64(1), frozenAmount) + + unfreezeMsg = NewUnfreezeMsg(acc.GetAddress(), "NNB-000M", 1) + sdkResult = handler(ctx, unfreezeMsg) + require.Equal(t, true, sdkResult.Code.IsOK()) + account = accountKeeper.GetAccount(ctx, msg.From).(types.NamedAccount) + amount = account.GetCoins().AmountOf("NNB-000M") + frozenAmount = account.GetFrozenCoins().AmountOf("NNB-000M") + require.Equal(t, int64(10000e8), amount) + require.Equal(t, int64(0), frozenAmount) + +} + +func TestHandleFreeze(t *testing.T) { + setChainVersion() + defer resetChainVersion() + ctx, handler, issueHandler, accountKeeper, tokenMapper := setup() + _, acc := testutils.NewAccount(ctx, accountKeeper, 100e8) + + ctx = ctx.WithValue(baseapp.TxHashKey, "000") + msg := issue.NewIssueMsg(acc.GetAddress(), "New BNB", "NNB", 10000e8, false) + sdkResult := issueHandler(ctx, msg) + require.Equal(t, true, sdkResult.Code.IsOK()) + + _, err := tokenMapper.GetToken(ctx, "NNB-000M") + require.NotNil(t, err) + require.Contains(t, err.Error(), "token(NNB-000M) not found") + + account := accountKeeper.GetAccount(ctx, msg.From).(types.NamedAccount) + amount := account.GetCoins().AmountOf("NNB-000") + frozenAmount := account.GetFrozenCoins().AmountOf("NNB-000") + require.Equal(t, int64(10000e8), amount) + require.Equal(t, int64(0), frozenAmount) + + _, err = tokenMapper.GetToken(ctx, "NNB-000") + require.Equal(t, true, sdkResult.Code.IsOK()) + + ctx = ctx.WithValue(baseapp.TxHashKey, "002") + freezeMsg := NewFreezeMsg(acc.GetAddress(), "NNB-000", 10001e8) + sdkResult = handler(ctx, freezeMsg) + require.Equal(t, false, sdkResult.Code.IsOK()) + require.Contains(t, sdkResult.Log, "do not have enough token to freeze") + + ctx = ctx.WithValue(baseapp.TxHashKey, "002") + freezeMsg = NewFreezeMsg(acc.GetAddress(), "NNB-000", 9999e8+1) + sdkResult = handler(ctx, freezeMsg) + require.Equal(t, true, sdkResult.Code.IsOK()) + + account = accountKeeper.GetAccount(ctx, msg.From).(types.NamedAccount) + amount = account.GetCoins().AmountOf("NNB-000") + frozenAmount = account.GetFrozenCoins().AmountOf("NNB-000") + require.Equal(t, int64(1e8-1), amount) + require.Equal(t, int64(9999e8+1), frozenAmount) + + token, err := tokenMapper.GetToken(ctx, "NNB-000") + require.NoError(t, err) + expectedToken, err := types.NewToken("New BNB", "NNB-000", 10000e8, acc.GetAddress(), false) + require.Equal(t, expectedToken, token) + + ctx = ctx.WithValue(baseapp.TxHashKey, "003") + unfreezeMsg := NewUnfreezeMsg(acc.GetAddress(), "NNB-000", 1) + sdkResult = handler(ctx, unfreezeMsg) + require.Equal(t, true, sdkResult.Code.IsOK()) + + unfreezeMsg = NewUnfreezeMsg(acc.GetAddress(), "NNB-000", 9999e8) + sdkResult = handler(ctx, unfreezeMsg) + require.Equal(t, true, sdkResult.Code.IsOK()) + account = accountKeeper.GetAccount(ctx, msg.From).(types.NamedAccount) + amount = account.GetCoins().AmountOf("NNB-000") + frozenAmount = account.GetFrozenCoins().AmountOf("NNB-000") + require.Equal(t, int64(10000e8), amount) + require.Equal(t, int64(0), frozenAmount) + +} diff --git a/plugins/tokens/freeze/msg.go b/plugins/tokens/freeze/msg.go index 4884af8cd..59ee7c7ea 100644 --- a/plugins/tokens/freeze/msg.go +++ b/plugins/tokens/freeze/msg.go @@ -7,6 +7,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/binance-chain/node/common/types" + "github.com/binance-chain/node/common/upgrade" ) // TODO: "route expressions can only contain alphanumeric characters", we need to change the cosmos sdk to support slash @@ -40,8 +41,15 @@ func (msg FreezeMsg) GetSigners() []sdk.AccAddress { return []sdk.AccA // ValidateBasic does a simple validation check that // doesn't require access to any other information. func (msg FreezeMsg) ValidateBasic() sdk.Error { + if sdk.IsUpgrade(upgrade.BEP8) && types.IsValidMiniTokenSymbol(msg.Symbol) { + if msg.Amount <= 0 { + return sdk.ErrInsufficientFunds("amount should be more than 0") + } + return nil + } + // if BEP8 not upgraded, we rely on `ValidateTokenSymbol` rejecting the MiniToken. // expect all msgs that reference a token after issue to use the suffixed form (e.g. "BNB-ABC") - err := types.ValidateMapperTokenSymbol(msg.Symbol) + err := types.ValidateTokenSymbol(msg.Symbol) if err != nil { return sdk.ErrInvalidCoins(err.Error()) } @@ -82,8 +90,15 @@ func (msg UnfreezeMsg) GetInvolvedAddresses() []sdk.AccAddress { return msg.GetS func (msg UnfreezeMsg) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.From} } func (msg UnfreezeMsg) ValidateBasic() sdk.Error { + if sdk.IsUpgrade(upgrade.BEP8) && types.IsValidMiniTokenSymbol(msg.Symbol) { + if msg.Amount <= 0 { + return sdk.ErrInsufficientFunds("amount should be more than 0") + } + return nil + } + // if BEP8 not upgraded, we rely on `ValidateTokenSymbol` rejecting the MiniToken. // expect all msgs that reference a token after issue to use the suffixed form (e.g. "BNB-ABC") - err := types.ValidateMapperTokenSymbol(msg.Symbol) + err := types.ValidateTokenSymbol(msg.Symbol) if err != nil { return sdk.ErrInvalidCoins(err.Error()) } diff --git a/plugins/tokens/genesis.go b/plugins/tokens/genesis.go index 0e7524326..473821e89 100644 --- a/plugins/tokens/genesis.go +++ b/plugins/tokens/genesis.go @@ -44,7 +44,7 @@ func InitGenesis(ctx sdk.Context, tokenMapper store.Mapper, coinKeeper bank.Keep if err != nil { panic(err) } - err = tokenMapper.NewToken(ctx, *token) + err = tokenMapper.NewToken(ctx, token) if err != nil { panic(err) } diff --git a/plugins/tokens/issue/handler.go b/plugins/tokens/issue/handler.go index 41286112b..c48c41b93 100644 --- a/plugins/tokens/issue/handler.go +++ b/plugins/tokens/issue/handler.go @@ -11,6 +11,7 @@ import ( "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/bank" + tmlog "github.com/tendermint/tendermint/libs/log" "github.com/binance-chain/node/common/log" "github.com/binance-chain/node/common/types" @@ -26,6 +27,10 @@ func NewHandler(tokenMapper store.Mapper, keeper bank.Keeper) sdk.Handler { return handleIssueToken(ctx, tokenMapper, keeper, msg) case MintMsg: return handleMintToken(ctx, tokenMapper, keeper, msg) + case IssueMiniMsg: + return handleIssueMiniToken(ctx, tokenMapper, keeper, msg) + case IssueTinyMsg: + return handleIssueTinyToken(ctx, tokenMapper, keeper, msg) default: errMsg := "Unrecognized msg type: " + reflect.TypeOf(msg).Name() return sdk.ErrUnknownRequest(errMsg).Result() @@ -37,29 +42,15 @@ func handleIssueToken(ctx sdk.Context, tokenMapper store.Mapper, bankKeeper bank errLogMsg := "issue token failed" symbol := strings.ToUpper(msg.Symbol) logger := log.With("module", "token", "symbol", symbol, "name", msg.Name, "total_supply", msg.TotalSupply, "issuer", msg.From) - var suffix string - - // TxHashKey is set in BaseApp's runMsgs - txHash := ctx.Value(baseapp.TxHashKey) - if txHashStr, ok := txHash.(string); ok { - if len(txHashStr) >= types.TokenSymbolTxHashSuffixLen { - suffix = txHashStr[:types.TokenSymbolTxHashSuffixLen] - } else { - logger.Error(errLogMsg, - "reason", fmt.Sprintf("%s on Context had a length of %d, expected >= %d", - baseapp.TxHashKey, len(txHashStr), types.TokenSymbolTxHashSuffixLen)) - return sdk.ErrInternal(fmt.Sprintf("unable to get the %s from Context", baseapp.TxHashKey)).Result() - } - } else { - logger.Error(errLogMsg, - "reason", fmt.Sprintf("%s on Context is not a string as expected", baseapp.TxHashKey)) + suffix, err := getTokenSuffix(ctx) + if err != nil { + logger.Error(errLogMsg, "reason", err.Error()) return sdk.ErrInternal(fmt.Sprintf("unable to get the %s from Context", baseapp.TxHashKey)).Result() } // the symbol is suffixed with the first n bytes of the tx hash symbol = fmt.Sprintf("%s-%s", symbol, suffix) - - if exists := tokenMapper.Exists(ctx, symbol); exists { + if exists := tokenMapper.ExistsBEP2(ctx, symbol); exists { logger.Info(errLogMsg, "reason", "already exists") return sdk.ErrInvalidCoins(fmt.Sprintf("symbol(%s) already exists", msg.Symbol)).Result() } @@ -69,35 +60,10 @@ func handleIssueToken(ctx sdk.Context, tokenMapper store.Mapper, bankKeeper bank logger.Error(errLogMsg, "reason", "create token failed: "+err.Error()) return sdk.ErrInternal(fmt.Sprintf("unable to create token struct: %s", err.Error())).Result() } - - if err := tokenMapper.NewToken(ctx, *token); err != nil { - logger.Error(errLogMsg, "reason", "add token failed: "+err.Error()) - return sdk.ErrInvalidCoins(err.Error()).Result() - } - - if _, _, sdkError := bankKeeper.AddCoins(ctx, token.Owner, - sdk.Coins{{ - Denom: token.Symbol, - Amount: token.TotalSupply.ToInt64(), - }}); sdkError != nil { - logger.Error(errLogMsg, "reason", "update balance failed: "+sdkError.Error()) - return sdkError.Result() - } - - serialized, err := json.Marshal(token) - if err != nil { - logger.Error(errLogMsg, "reason", "fatal! unable to json serialize token: "+err.Error()) - panic(err) // fatal, the sky is falling in goland - } - - logger.Info("finished issuing token") - - return sdk.Result{ - Data: serialized, - Log: fmt.Sprintf("Issued %s", token.Symbol), - } + return issue(ctx, logger, tokenMapper, bankKeeper, token) } +//Mint MiniToken is also handled by this function func handleMintToken(ctx sdk.Context, tokenMapper store.Mapper, bankKeeper bank.Keeper, msg MintMsg) sdk.Result { symbol := strings.ToUpper(msg.Symbol) logger := log.With("module", "token", "symbol", symbol, "amount", msg.Amount, "minter", msg.From) @@ -109,7 +75,7 @@ func handleMintToken(ctx sdk.Context, tokenMapper store.Mapper, bankKeeper bank. return sdk.ErrInvalidCoins(fmt.Sprintf("symbol(%s) does not exist", msg.Symbol)).Result() } - if !token.Mintable { + if !token.IsMintable() { logger.Info(errLogMsg, "reason", "token cannot be minted") return sdk.ErrInvalidCoins(fmt.Sprintf("token(%s) cannot be minted", msg.Symbol)).Result() } @@ -119,22 +85,33 @@ func handleMintToken(ctx sdk.Context, tokenMapper store.Mapper, bankKeeper bank. return sdk.ErrUnauthorized(fmt.Sprintf("only the owner can mint token %s", msg.Symbol)).Result() } - // use minus to prevent overflow - if msg.Amount > common.TokenMaxTotalSupply-token.TotalSupply.ToInt64() { - logger.Info(errLogMsg, "reason", "exceed the max total supply") - return sdk.ErrInvalidCoins(fmt.Sprintf("mint amount is too large, the max total supply is %ds", - common.TokenMaxTotalSupply)).Result() + if common.IsMiniTokenSymbol(symbol) { + miniToken := token.(*types.MiniToken) + // use minus to prevent overflow + if msg.Amount > miniToken.TokenType.UpperBound()-miniToken.TotalSupply.ToInt64() { + logger.Info(errLogMsg, "reason", "total supply exceeds the max total supply") + return sdk.ErrInvalidCoins(fmt.Sprintf("mint amount is too large, the max total supply is %d", + miniToken.TokenType.UpperBound())).Result() + } + } else { + // use minus to prevent overflow + if msg.Amount > common.TokenMaxTotalSupply-token.GetTotalSupply().ToInt64() { + logger.Info(errLogMsg, "reason", "exceed the max total supply") + return sdk.ErrInvalidCoins(fmt.Sprintf("mint amount is too large, the max total supply is %ds", + common.TokenMaxTotalSupply)).Result() + } } - newTotalSupply := token.TotalSupply.ToInt64() + msg.Amount + + newTotalSupply := token.GetTotalSupply().ToInt64() + msg.Amount err = tokenMapper.UpdateTotalSupply(ctx, symbol, newTotalSupply) if err != nil { logger.Error(errLogMsg, "reason", "update total supply failed: "+err.Error()) return sdk.ErrInternal(fmt.Sprintf("update total supply failed")).Result() } - _, _, sdkError := bankKeeper.AddCoins(ctx, token.Owner, + _, _, sdkError := bankKeeper.AddCoins(ctx, token.GetOwner(), sdk.Coins{{ - Denom: token.Symbol, + Denom: token.GetSymbol(), Amount: msg.Amount, }}) if sdkError != nil { @@ -147,3 +124,51 @@ func handleMintToken(ctx sdk.Context, tokenMapper store.Mapper, bankKeeper bank. Data: []byte(strconv.FormatInt(newTotalSupply, 10)), } } + +func issue(ctx sdk.Context, logger tmlog.Logger, tokenMapper store.Mapper, bankKeeper bank.Keeper, token common.IToken) sdk.Result { + errLogMsg := "issue token failed" + if err := tokenMapper.NewToken(ctx, token); err != nil { + logger.Error(errLogMsg, "reason", "add token failed: "+err.Error()) + return sdk.ErrInvalidCoins(err.Error()).Result() + } + + if _, _, sdkError := bankKeeper.AddCoins(ctx, token.GetOwner(), + sdk.Coins{{ + Denom: token.GetSymbol(), + Amount: token.GetTotalSupply().ToInt64(), + }}); sdkError != nil { + logger.Error(errLogMsg, "reason", "update balance failed: "+sdkError.Error()) + return sdkError.Result() + } + + serialized, err := json.Marshal(token) + if err != nil { + logger.Error(errLogMsg, "reason", "fatal! unable to json serialize token: "+err.Error()) + panic(err) // fatal, the sky is falling in goland + } + + logger.Info("finished issuing token") + + return sdk.Result{ + Data: serialized, + Log: fmt.Sprintf("Issued %s", token.GetSymbol()), + } +} + +func getTokenSuffix(ctx sdk.Context) (suffix string, err error) { + // TxHashKey is set in BaseApp's runMsgs + txHash := ctx.Value(baseapp.TxHashKey) + if txHashStr, ok := txHash.(string); ok { + if len(txHashStr) >= types.TokenSymbolTxHashSuffixLen { + suffix = txHashStr[:types.TokenSymbolTxHashSuffixLen] + return suffix, nil + } else { + err = fmt.Errorf("%s on Context had a length of %d, expected >= %d", + baseapp.TxHashKey, len(txHashStr), types.TokenSymbolTxHashSuffixLen) + return "", err + } + } else { + err = fmt.Errorf("%s on Context is not a string as expected", baseapp.TxHashKey) + return "", err + } +} diff --git a/plugins/tokens/issue/handler_mini.go b/plugins/tokens/issue/handler_mini.go new file mode 100644 index 000000000..55b21ec12 --- /dev/null +++ b/plugins/tokens/issue/handler_mini.go @@ -0,0 +1,55 @@ +package issue + +import ( + "fmt" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/bank" + + "github.com/binance-chain/node/common/log" + common "github.com/binance-chain/node/common/types" + "github.com/binance-chain/node/plugins/tokens/store" +) + +func handleIssueMiniToken(ctx sdk.Context, tokenMapper store.Mapper, bankKeeper bank.Keeper, msg IssueMiniMsg) sdk.Result { + errLogMsg := "issue miniToken failed" + origSymbol := strings.ToUpper(msg.Symbol) + logger := log.With("module", "mini-token", "symbol", origSymbol, "name", msg.Name, "total_supply", msg.TotalSupply, "issuer", msg.From) + + suffix, err := getTokenSuffix(ctx) + if err != nil { + logger.Error(errLogMsg, "reason", err.Error()) + } + suffix += common.MiniTokenSymbolMSuffix + symbol := fmt.Sprintf("%s-%s", origSymbol, suffix) + + if exists := tokenMapper.ExistsMini(ctx, symbol); exists { + logger.Info(errLogMsg, "reason", "already exists") + return sdk.ErrInvalidCoins(fmt.Sprintf("symbol(%s) already exists", msg.Symbol)).Result() + } + + token := common.NewMiniToken(msg.Name, origSymbol, symbol, common.MiniRangeType, msg.TotalSupply, msg.From, msg.Mintable, msg.TokenURI) + return issue(ctx, logger, tokenMapper, bankKeeper, token) +} + +func handleIssueTinyToken(ctx sdk.Context, tokenMapper store.Mapper, bankKeeper bank.Keeper, msg IssueTinyMsg) sdk.Result { + errLogMsg := "issue tinyToken failed" + origSymbol := strings.ToUpper(msg.Symbol) + logger := log.With("module", "mini-token", "symbol", origSymbol, "name", msg.Name, "total_supply", msg.TotalSupply, "issuer", msg.From) + + suffix, err := getTokenSuffix(ctx) + if err != nil { + logger.Error(errLogMsg, "reason", err.Error()) + } + suffix += common.MiniTokenSymbolMSuffix + symbol := fmt.Sprintf("%s-%s", origSymbol, suffix) + + if exists := tokenMapper.ExistsMini(ctx, symbol); exists { + logger.Info(errLogMsg, "reason", "already exists") + return sdk.ErrInvalidCoins(fmt.Sprintf("symbol(%s) already exists", msg.Symbol)).Result() + } + + token := common.NewMiniToken(msg.Name, origSymbol, symbol, common.TinyRangeType, msg.TotalSupply, msg.From, msg.Mintable, msg.TokenURI) + return issue(ctx, logger, tokenMapper, bankKeeper, token) +} diff --git a/plugins/tokens/issue/handler_mini_test.go b/plugins/tokens/issue/handler_mini_test.go new file mode 100644 index 000000000..a1e1f0abd --- /dev/null +++ b/plugins/tokens/issue/handler_mini_test.go @@ -0,0 +1,140 @@ +package issue + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/bank" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + + "github.com/binance-chain/node/common/testutils" + "github.com/binance-chain/node/common/types" + "github.com/binance-chain/node/plugins/tokens/store" + "github.com/binance-chain/node/wire" +) + +func setupMini() (sdk.Context, sdk.Handler, auth.AccountKeeper, store.Mapper) { + ms, capKey1, capKey2 := testutils.SetupMultiStoreForUnitTest() + cdc := wire.NewCodec() + cdc.RegisterInterface((*types.IToken)(nil), nil) + cdc.RegisterConcrete(&types.Token{}, "bnbchain/Token", nil) + cdc.RegisterConcrete(&types.MiniToken{}, "bnbchain/MiniToken", nil) + tokenMapper := store.NewMapper(cdc, capKey1) + accountKeeper := auth.NewAccountKeeper(cdc, capKey2, auth.ProtoBaseAccount) + bankKeeper := bank.NewBaseKeeper(accountKeeper) + handler := NewHandler(tokenMapper, bankKeeper) + + accountStore := ms.GetKVStore(capKey2) + accountStoreCache := auth.NewAccountStoreCache(cdc, accountStore, 10) + ctx := sdk.NewContext(ms, abci.Header{ChainID: "mychainid", Height: 1}, + sdk.RunTxModeDeliver, log.NewNopLogger()). + WithAccountCache(auth.NewAccountCache(accountStoreCache)) + return ctx, handler, accountKeeper, tokenMapper +} + +func TestHandleIssueMiniToken(t *testing.T) { + setChainVersion() + defer resetChainVersion() + ctx, handler, accountKeeper, tokenMapper := setupMini() + _, acc := testutils.NewAccount(ctx, accountKeeper, 100e8) + + ctx = ctx.WithValue(baseapp.TxHashKey, "000") + msg := NewIssueTinyMsg(acc.GetAddress(), "New BNB", "NNB", 10000e8+100, false, "http://www.xyz.com/nnb.json") + sdkResult := msg.ValidateBasic().Result() + require.Equal(t, false, sdkResult.Code.IsOK()) + require.Contains(t, sdkResult.Log, fmt.Sprintf("total supply should be between %d and %d", types.MiniTokenMinExecutionAmount, types.TinyRangeType.UpperBound())) + + ctx = ctx.WithValue(baseapp.TxHashKey, "000") + msg = NewIssueTinyMsg(acc.GetAddress(), "New BNB", "NNB", 10000e8, false, "http://www.xyz.com/nnb.json") + sdkResult = handler(ctx, msg) + require.Equal(t, true, sdkResult.Code.IsOK()) + + token, err := tokenMapper.GetToken(ctx, "NNB-000M") + require.NoError(t, err) + expectedToken := types.NewMiniToken("New BNB", "NNB", "NNB-000M", 1, 10000e8, acc.GetAddress(), false, "http://www.xyz.com/nnb.json") + require.Equal(t, expectedToken, token) + + sdkResult = handler(ctx, msg) + require.Contains(t, sdkResult.Log, "symbol(NNB) already exists") + + ctx = ctx.WithValue(baseapp.TxHashKey, "002") + msgMini := NewIssueMiniMsg(acc.GetAddress(), "New BB", "NBB", 1000000e8+100, false, "http://www.xyz.com/nnb.json") + sdkResult = msgMini.ValidateBasic().Result() + require.Equal(t, false, sdkResult.Code.IsOK()) + require.Contains(t, sdkResult.Log, fmt.Sprintf("total supply should be between %d and %d", types.MiniTokenMinExecutionAmount, types.MiniRangeType.UpperBound())) + + ctx = ctx.WithValue(baseapp.TxHashKey, "002") + msgMini = NewIssueMiniMsg(acc.GetAddress(), "New BB", "NBB", 10000e8+100, false, "http://www.xyz.com/nnb.json") + sdkResult = handler(ctx, msgMini) + require.Equal(t, true, sdkResult.Code.IsOK()) + + token, err = tokenMapper.GetToken(ctx, "NBB-002M") + require.NoError(t, err) + expectedToken = types.NewMiniToken("New BB", "NBB", "NBB-002M", 2, 10000e8+100, acc.GetAddress(), false, "http://www.xyz.com/nnb.json") + require.Equal(t, expectedToken, token) +} + +func TestHandleMintMiniToken(t *testing.T) { + setChainVersion() + defer resetChainVersion() + ctx, handler, accountKeeper, tokenMapper := setup() + _, acc := testutils.NewAccount(ctx, accountKeeper, 100e8) + mintMsg := NewMintMsg(acc.GetAddress(), "NNB-000M", 1001e8) + sdkResult := handler(ctx, mintMsg) + require.Contains(t, sdkResult.Log, "symbol(NNB-000M) does not exist") + + issueMsg := NewIssueTinyMsg(acc.GetAddress(), "New BNB", "NNB", 9000e8, true, "http://www.xyz.com/nnb.json") + ctx = ctx.WithValue(baseapp.TxHashKey, "000") + sdkResult = handler(ctx, issueMsg) + require.Equal(t, true, sdkResult.Code.IsOK()) + + sdkResult = handler(ctx, mintMsg) + require.Equal(t, false, sdkResult.Code.IsOK()) + require.Contains(t, sdkResult.Log, fmt.Sprintf("mint amount is too large, the max total supply is %d", types.TinyRangeType.UpperBound())) + + token, err := tokenMapper.GetToken(ctx, "NNB-000M") + require.NoError(t, err) + expectedToken := types.NewMiniToken("New BNB", "NNB", "NNB-000M", 1, 9000e8, acc.GetAddress(), true, "http://www.xyz.com/nnb.json") + require.Equal(t, expectedToken, token) + + _, err = tokenMapper.GetToken(ctx, "NNB-000") + require.NotNil(t, err) + require.Contains(t, err.Error(), "token(NNB-000) not found") + + sdkResult = handler(ctx, mintMsg) + require.Equal(t, false, sdkResult.Code.IsOK()) + require.Contains(t, sdkResult.Log, "mint amount is too large") + + validMintMsg := NewMintMsg(acc.GetAddress(), "NNB-000M", 1000e8) + sdkResult = handler(ctx, validMintMsg) + require.Equal(t, true, sdkResult.Code.IsOK()) + token, err = tokenMapper.GetToken(ctx, "NNB-000M") + expectedToken = types.NewMiniToken("New BNB", "NNB", "NNB-000M", 1, 10000e8, acc.GetAddress(), true, "http://www.xyz.com/nnb.json") + require.Equal(t, expectedToken, token) + + _, acc2 := testutils.NewAccount(ctx, accountKeeper, 100e8) + invalidMintMsg := NewMintMsg(acc2.GetAddress(), "NNB-000M", 100e8) + sdkResult = handler(ctx, invalidMintMsg) + require.Contains(t, sdkResult.Log, "only the owner can mint token NNB") + + // issue a non-mintable token + issueMsg = NewIssueTinyMsg(acc.GetAddress(), "New BNB2", "NNB2", 9000e8, false, "http://www.xyz.com/nnb.json") + ctx = ctx.WithValue(baseapp.TxHashKey, "000") + sdkResult = handler(ctx, issueMsg) + require.Equal(t, true, sdkResult.Code.IsOK()) + + mintMsg = NewMintMsg(acc.GetAddress(), "NNB2-000M", 1000e8) + sdkResult = handler(ctx, mintMsg) + require.Contains(t, sdkResult.Log, "token(NNB2-000M) cannot be minted") + + // mint native token + invalidMintMsg = NewMintMsg(acc.GetAddress(), "BNB", 10000e8) + require.Contains(t, invalidMintMsg.ValidateBasic().Error(), "cannot mint native token") +} diff --git a/plugins/tokens/issue/handler_test.go b/plugins/tokens/issue/handler_test.go index e5a4cfc7b..7ef4277c2 100644 --- a/plugins/tokens/issue/handler_test.go +++ b/plugins/tokens/issue/handler_test.go @@ -15,18 +15,21 @@ import ( "github.com/binance-chain/node/common/testutils" "github.com/binance-chain/node/common/types" + "github.com/binance-chain/node/common/upgrade" "github.com/binance-chain/node/plugins/tokens/store" "github.com/binance-chain/node/wire" ) func setup() (sdk.Context, sdk.Handler, auth.AccountKeeper, store.Mapper) { - ms, capKey1, capKey2 := testutils.SetupMultiStoreForUnitTest() + ms, capKey1, capKey2, _ := testutils.SetupThreeMultiStoreForUnitTest() cdc := wire.NewCodec() + cdc.RegisterInterface((*types.IToken)(nil), nil) + cdc.RegisterConcrete(&types.Token{}, "bnbchain/Token", nil) + cdc.RegisterConcrete(&types.MiniToken{}, "bnbchain/MiniToken", nil) tokenMapper := store.NewMapper(cdc, capKey1) accountKeeper := auth.NewAccountKeeper(cdc, capKey2, auth.ProtoBaseAccount) bankKeeper := bank.NewBaseKeeper(accountKeeper) handler := NewHandler(tokenMapper, bankKeeper) - accountStore := ms.GetKVStore(capKey2) accountStoreCache := auth.NewAccountStoreCache(cdc, accountStore, 10) ctx := sdk.NewContext(ms, abci.Header{ChainID: "mychainid", Height: 1}, @@ -35,6 +38,14 @@ func setup() (sdk.Context, sdk.Handler, auth.AccountKeeper, store.Mapper) { return ctx, handler, accountKeeper, tokenMapper } +func setChainVersion() { + upgrade.Mgr.AddUpgradeHeight(upgrade.BEP8, -1) +} + +func resetChainVersion() { + upgrade.Mgr.Config.HeightMap = nil +} + func TestHandleIssueToken(t *testing.T) { ctx, handler, accountKeeper, tokenMapper := setup() _, acc := testutils.NewAccount(ctx, accountKeeper, 100e8) @@ -46,7 +57,7 @@ func TestHandleIssueToken(t *testing.T) { token, err := tokenMapper.GetToken(ctx, "NNB-000") require.NoError(t, err) expectedToken, err := types.NewToken("New BNB", "NNB-000", 100000e8, acc.GetAddress(), false) - require.Equal(t, *expectedToken, token) + require.Equal(t, expectedToken, token) sdkResult = handler(ctx, msg) require.Contains(t, sdkResult.Log, "symbol(NNB) already exists") @@ -70,7 +81,7 @@ func TestHandleMintToken(t *testing.T) { token, err := tokenMapper.GetToken(ctx, "NNB-000") require.NoError(t, err) expectedToken, err := types.NewToken("New BNB", "NNB-000", 110000e8, acc.GetAddress(), true) - require.Equal(t, *expectedToken, token) + require.Equal(t, expectedToken, token) invalidMintMsg := NewMintMsg(acc.GetAddress(), "NNB-000", types.TokenMaxTotalSupply) sdkResult = handler(ctx, invalidMintMsg) diff --git a/plugins/tokens/issue/msg.go b/plugins/tokens/issue/msg.go index 9a454c66f..412cd17f5 100644 --- a/plugins/tokens/issue/msg.go +++ b/plugins/tokens/issue/msg.go @@ -7,6 +7,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/binance-chain/node/common/types" + "github.com/binance-chain/node/common/upgrade" ) // TODO: "route expressions can only contain alphanumeric characters", we need to change the cosmos sdk to support slash @@ -46,7 +47,7 @@ func (msg IssueMsg) ValidateBasic() sdk.Error { return sdk.ErrInvalidAddress("sender address cannot be empty") } - if err := types.ValidateIssueMsgTokenSymbol(msg.Symbol); err != nil { + if err := types.ValidateIssueSymbol(msg.Symbol); err != nil { return sdk.ErrInvalidCoins(err.Error()) } @@ -96,7 +97,15 @@ func (msg MintMsg) ValidateBasic() sdk.Error { return sdk.ErrInvalidAddress("sender address cannot be empty") } - if err := types.ValidateMapperTokenSymbol(msg.Symbol); err != nil { + if sdk.IsUpgrade(upgrade.BEP8) && types.IsValidMiniTokenSymbol(msg.Symbol) { + if msg.Amount < types.MiniTokenMinExecutionAmount { + return sdk.ErrInvalidCoins(fmt.Sprintf("mint amount should be no less than %d", types.MiniTokenMinExecutionAmount)) + } + return nil + } + + // if BEP8 not upgraded, we rely on `ValidateTokenSymbol` rejecting the MiniToken. + if err := types.ValidateTokenSymbol(msg.Symbol); err != nil { return sdk.ErrInvalidCoins(err.Error()) } diff --git a/plugins/tokens/issue/msg_mini.go b/plugins/tokens/issue/msg_mini.go new file mode 100644 index 000000000..ba322b0f6 --- /dev/null +++ b/plugins/tokens/issue/msg_mini.go @@ -0,0 +1,80 @@ +package issue + +import ( + "encoding/json" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/binance-chain/node/common/types" +) + +// TODO: "route expressions can only contain alphanumeric characters", we need to change the cosmos sdk to support slash +// const Route = "tokens/issue" +const ( + IssueMiniMsgType = "miniIssueMsg" //For max total supply in range 2 +) + +var _ sdk.Msg = IssueMiniMsg{} + +type IssueMiniMsg struct { + From sdk.AccAddress `json:"from"` + Name string `json:"name"` + Symbol string `json:"symbol"` + TotalSupply int64 `json:"total_supply"` + Mintable bool `json:"mintable"` + TokenURI string `json:"token_uri"` +} + +func NewIssueMiniMsg(from sdk.AccAddress, name, symbol string, supply int64, mintable bool, tokenURI string) IssueMiniMsg { + return IssueMiniMsg{ + From: from, + Name: name, + Symbol: symbol, + TotalSupply: supply, + Mintable: mintable, + TokenURI: tokenURI, + } +} + +// ValidateBasic does a simple validation check that +// doesn't require access to any other information. +func (msg IssueMiniMsg) ValidateBasic() sdk.Error { + if msg.From == nil { + return sdk.ErrInvalidAddress("sender address cannot be empty") + } + + if err := types.ValidateIssueMiniSymbol(msg.Symbol); err != nil { + return sdk.ErrInvalidCoins(err.Error()) + } + + if len(msg.Name) == 0 || len(msg.Name) > maxTokenNameLength { + return sdk.ErrInvalidCoins(fmt.Sprintf("token name should have 1 ~ %v characters", maxTokenNameLength)) + } + + if len(msg.TokenURI) > types.MaxTokenURILength { + return sdk.ErrInvalidCoins(fmt.Sprintf("token seturi should not exceed %v characters", types.MaxTokenURILength)) + } + + if msg.TotalSupply < types.MiniTokenMinExecutionAmount || msg.TotalSupply > types.MiniRangeType.UpperBound() { + return sdk.ErrInvalidCoins(fmt.Sprintf("total supply should be between %d and %d", types.MiniTokenMinExecutionAmount, types.MiniRangeType.UpperBound())) + } + + return nil +} + +// Implements IssueMiniMsg. +func (msg IssueMiniMsg) Route() string { return Route } +func (msg IssueMiniMsg) Type() string { return IssueMiniMsgType } +func (msg IssueMiniMsg) String() string { return fmt.Sprintf("IssueMiniMsg{%#v}", msg) } +func (msg IssueMiniMsg) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.From} } +func (msg IssueMiniMsg) GetSignBytes() []byte { + b, err := json.Marshal(msg) // XXX: ensure some canonical form + if err != nil { + panic(err) + } + return b +} +func (msg IssueMiniMsg) GetInvolvedAddresses() []sdk.AccAddress { + return msg.GetSigners() +} diff --git a/plugins/tokens/issue/msg_tiny.go b/plugins/tokens/issue/msg_tiny.go new file mode 100644 index 000000000..eda4ac925 --- /dev/null +++ b/plugins/tokens/issue/msg_tiny.go @@ -0,0 +1,80 @@ +package issue + +import ( + "encoding/json" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/binance-chain/node/common/types" +) + +// TODO: "route expressions can only contain alphanumeric characters", we need to change the cosmos sdk to support slash +// const Route = "tokens/issue" +const ( + IssueTinyMsgType = "tinyIssueMsg" +) + +var _ sdk.Msg = IssueTinyMsg{} + +type IssueTinyMsg struct { + From sdk.AccAddress `json:"from"` + Name string `json:"name"` + Symbol string `json:"symbol"` + TotalSupply int64 `json:"total_supply"` + Mintable bool `json:"mintable"` + TokenURI string `json:"token_uri"` +} + +func NewIssueTinyMsg(from sdk.AccAddress, name, symbol string, supply int64, mintable bool, tokenURI string) IssueTinyMsg { + return IssueTinyMsg{ + From: from, + Name: name, + Symbol: symbol, + TotalSupply: supply, + Mintable: mintable, + TokenURI: tokenURI, + } +} + +// ValidateBasic does a simple validation check that +// doesn't require access to any other information. +func (msg IssueTinyMsg) ValidateBasic() sdk.Error { + if msg.From == nil { + return sdk.ErrInvalidAddress("sender address cannot be empty") + } + + if err := types.ValidateIssueMiniSymbol(msg.Symbol); err != nil { + return sdk.ErrInvalidCoins(err.Error()) + } + + if len(msg.Name) == 0 || len(msg.Name) > maxTokenNameLength { + return sdk.ErrInvalidCoins(fmt.Sprintf("token name should have 1 ~ %v characters", maxTokenNameLength)) + } + + if len(msg.TokenURI) > types.MaxTokenURILength { + return sdk.ErrInvalidCoins(fmt.Sprintf("token seturi should not exceed %v characters", types.MaxTokenURILength)) + } + + if msg.TotalSupply < types.MiniTokenMinExecutionAmount || msg.TotalSupply > types.TinyRangeType.UpperBound() { + return sdk.ErrInvalidCoins(fmt.Sprintf("total supply should be between %d and %d", types.MiniTokenMinExecutionAmount, types.TinyRangeType.UpperBound())) + } + + return nil +} + +// Implements IssueTinyMsg. +func (msg IssueTinyMsg) Route() string { return Route } +func (msg IssueTinyMsg) Type() string { return IssueTinyMsgType } +func (msg IssueTinyMsg) String() string { return fmt.Sprintf("IssueTinyMsg{%#v}", msg) } +func (msg IssueTinyMsg) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.From} } +func (msg IssueTinyMsg) GetSignBytes() []byte { + b, err := json.Marshal(msg) // XXX: ensure some canonical form + if err != nil { + panic(err) + } + return b +} +func (msg IssueTinyMsg) GetInvolvedAddresses() []sdk.AccAddress { + return msg.GetSigners() +} diff --git a/plugins/tokens/plugin.go b/plugins/tokens/plugin.go index b41bc6b32..c54c52bf9 100644 --- a/plugins/tokens/plugin.go +++ b/plugins/tokens/plugin.go @@ -15,6 +15,7 @@ import ( ) const abciQueryPrefix = "tokens" +const miniAbciQueryPrefix = "mini-tokens" // InitPlugin initializes the plugin. func InitPlugin( @@ -26,12 +27,14 @@ func InitPlugin( } // add abci handlers - handler := createQueryHandler(mapper) - appp.RegisterQueryHandler(abciQueryPrefix, handler) + tokenHandler := createQueryHandler(mapper, abciQueryPrefix) + miniTokenHandler := createQueryHandler(mapper, miniAbciQueryPrefix) + appp.RegisterQueryHandler(abciQueryPrefix, tokenHandler) + appp.RegisterQueryHandler(miniAbciQueryPrefix, miniTokenHandler) } -func createQueryHandler(mapper Mapper) app.AbciQueryHandler { - return createAbciQueryHandler(mapper) +func createQueryHandler(mapper Mapper, queryPrefix string) app.AbciQueryHandler { + return createAbciQueryHandler(mapper, queryPrefix) } // EndBreatheBlock processes the breathe block lifecycle event. diff --git a/plugins/tokens/route.go b/plugins/tokens/route.go index a65c3ac70..11ae1f784 100644 --- a/plugins/tokens/route.go +++ b/plugins/tokens/route.go @@ -8,6 +8,7 @@ import ( "github.com/binance-chain/node/plugins/tokens/burn" "github.com/binance-chain/node/plugins/tokens/freeze" "github.com/binance-chain/node/plugins/tokens/issue" + "github.com/binance-chain/node/plugins/tokens/seturi" "github.com/binance-chain/node/plugins/tokens/store" "github.com/binance-chain/node/plugins/tokens/swap" "github.com/binance-chain/node/plugins/tokens/timelock" @@ -21,5 +22,6 @@ func Routes(tokenMapper store.Mapper, accKeeper auth.AccountKeeper, keeper bank. routes[freeze.FreezeRoute] = freeze.NewHandler(tokenMapper, accKeeper, keeper) routes[timelock.MsgRoute] = timelock.NewHandler(timeLockKeeper) routes[swap.AtomicSwapRoute] = swap.NewHandler(swapKeeper) + routes[seturi.SetURIRoute] = seturi.NewHandler(tokenMapper) return routes } diff --git a/plugins/tokens/seturi/handler.go b/plugins/tokens/seturi/handler.go new file mode 100644 index 000000000..fd2a599cc --- /dev/null +++ b/plugins/tokens/seturi/handler.go @@ -0,0 +1,60 @@ +package seturi + +import ( + "fmt" + "reflect" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/binance-chain/node/common/log" + common "github.com/binance-chain/node/common/types" + "github.com/binance-chain/node/plugins/tokens/store" +) + +func NewHandler(tokenMapper store.Mapper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + switch msg := msg.(type) { + case SetURIMsg: + return handleSetURI(ctx, tokenMapper, msg) + default: + errMsg := "Unrecognized msg type: " + reflect.TypeOf(msg).Name() + return sdk.ErrUnknownRequest(errMsg).Result() + } + } +} + +func handleSetURI(ctx sdk.Context, tokenMapper store.Mapper, msg SetURIMsg) sdk.Result { + symbol := strings.ToUpper(msg.Symbol) + logger := log.With("module", "mini-token", "symbol", symbol, "tokenURI", msg.TokenURI, "from", msg.From) + + errLogMsg := "set token URI failed" + token, err := tokenMapper.GetToken(ctx, symbol) + if err != nil { + logger.Info(errLogMsg, "reason", "symbol not exist") + return sdk.ErrInvalidCoins(fmt.Sprintf("symbol(%s) does not exist", msg.Symbol)).Result() + } + + if !token.IsOwner(msg.From) { + logger.Info(errLogMsg, "reason", "not the token owner") + return sdk.ErrUnauthorized(fmt.Sprintf("only the owner can mint token %s", msg.Symbol)).Result() + } + + if len(msg.TokenURI) < 1 { + return sdk.ErrInvalidCoins(fmt.Sprintf("token uri should not be empty")).Result() + } + + if len(msg.TokenURI) > common.MaxTokenURILength { + return sdk.ErrInvalidCoins(fmt.Sprintf("token uri should not exceed %v characters", common.MaxTokenURILength)).Result() + } + err = tokenMapper.UpdateMiniTokenURI(ctx, symbol, msg.TokenURI) + if err != nil { + logger.Error(errLogMsg, "reason", "update token uri failed: "+err.Error()) + return sdk.ErrInternal(fmt.Sprintf("update token uri failed")).Result() + } + + logger.Info("finished update token uri") + return sdk.Result{ + Data: []byte(msg.TokenURI), + } +} diff --git a/plugins/tokens/seturi/handler_test.go b/plugins/tokens/seturi/handler_test.go new file mode 100644 index 000000000..b008dd540 --- /dev/null +++ b/plugins/tokens/seturi/handler_test.go @@ -0,0 +1,91 @@ +package seturi + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/bank" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + + "github.com/binance-chain/node/common/testutils" + "github.com/binance-chain/node/common/types" + "github.com/binance-chain/node/common/upgrade" + "github.com/binance-chain/node/plugins/tokens/issue" + "github.com/binance-chain/node/plugins/tokens/store" + "github.com/binance-chain/node/wire" +) + +func setup() (sdk.Context, sdk.Handler, sdk.Handler, auth.AccountKeeper, store.Mapper) { + ms, capKey1, capKey2 := testutils.SetupMultiStoreForUnitTest() + cdc := wire.NewCodec() + cdc.RegisterInterface((*types.IToken)(nil), nil) + cdc.RegisterConcrete(&types.Token{}, "bnbchain/Token", nil) + cdc.RegisterConcrete(&types.MiniToken{}, "bnbchain/MiniToken", nil) + tokenMapper := store.NewMapper(cdc, capKey1) + accountKeeper := auth.NewAccountKeeper(cdc, capKey2, auth.ProtoBaseAccount) + handler := NewHandler(tokenMapper) + + bankKeeper := bank.NewBaseKeeper(accountKeeper) + miniTokenHandler := issue.NewHandler(tokenMapper, bankKeeper) + + accountStore := ms.GetKVStore(capKey2) + accountStoreCache := auth.NewAccountStoreCache(cdc, accountStore, 10) + ctx := sdk.NewContext(ms, abci.Header{ChainID: "mychainid", Height: 1}, + sdk.RunTxModeDeliver, log.NewNopLogger()). + WithAccountCache(auth.NewAccountCache(accountStoreCache)) + return ctx, handler, miniTokenHandler, accountKeeper, tokenMapper +} + +func setChainVersion() { + upgrade.Mgr.AddUpgradeHeight(upgrade.BEP8, -1) +} + +func resetChainVersion() { + upgrade.Mgr.Config.HeightMap = nil +} + +func TestHandleSetURI(t *testing.T) { + setChainVersion() + defer resetChainVersion() + ctx, handler, miniIssueHandler, accountKeeper, tokenMapper := setup() + _, acc := testutils.NewAccount(ctx, accountKeeper, 100e8) + + ctx = ctx.WithValue(baseapp.TxHashKey, "000") + msg := issue.NewIssueMiniMsg(acc.GetAddress(), "New BNB", "NNB", 10000e8, false, "http://www.xyz.com/nnb.json") + sdkResult := miniIssueHandler(ctx, msg) + require.Equal(t, true, sdkResult.Code.IsOK()) + + token, err := tokenMapper.GetToken(ctx, "NNB-000M") + require.NoError(t, err) + expectedToken := types.NewMiniToken("New BNB", "NNB", "NNB-000M", 2, 10000e8, acc.GetAddress(), false, "http://www.xyz.com/nnb.json") + require.Equal(t, expectedToken, token) + + ctx = ctx.WithValue(baseapp.TxHashKey, "002") + setUriMsg := NewSetUriMsg(acc.GetAddress(), "NBB", "http://www.123.com/nnb_new.json") + sdkResult = handler(ctx, setUriMsg) + require.Equal(t, false, sdkResult.Code.IsOK()) + require.Contains(t, sdkResult.Log, "symbol(NBB) does not exist") + + ctx = ctx.WithValue(baseapp.TxHashKey, "002") + setUriMsg = NewSetUriMsg(acc.GetAddress(), "NNB-000M", "http://www.123.com/nnb_new.json") + sdkResult = handler(ctx, setUriMsg) + require.Equal(t, true, sdkResult.Code.IsOK()) + + token, err = tokenMapper.GetToken(ctx, "NNB-000M") + require.NoError(t, err) + expectedToken = types.NewMiniToken("New BNB", "NNB", "NNB-000M", 2, 10000e8, acc.GetAddress(), false, "http://www.123.com/nnb_new.json") + require.Equal(t, expectedToken, token) + + _, acc2 := testutils.NewAccount(ctx, accountKeeper, 100e8) + ctx = ctx.WithValue(baseapp.TxHashKey, "002") + setUriMsg = NewSetUriMsg(acc2.GetAddress(), "NNB-000M", "http://www.124.com/nnb_new.json") + sdkResult = handler(ctx, setUriMsg) + require.Equal(t, false, sdkResult.Code.IsOK()) + require.Contains(t, sdkResult.Log, "only the owner can mint token") +} diff --git a/plugins/tokens/seturi/msg.go b/plugins/tokens/seturi/msg.go new file mode 100644 index 000000000..c3e161a3d --- /dev/null +++ b/plugins/tokens/seturi/msg.go @@ -0,0 +1,56 @@ +package seturi + +import ( + "encoding/json" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/binance-chain/node/common/types" +) + +const SetURIRoute = "miniTokensSetURI" + +var _ sdk.Msg = SetURIMsg{} + +type SetURIMsg struct { + From sdk.AccAddress `json:"from"` + Symbol string `json:"symbol"` + TokenURI string `json:"token_uri"` +} + +func NewSetUriMsg(from sdk.AccAddress, symbol string, tokenURI string) SetURIMsg { + return SetURIMsg{ + From: from, + Symbol: symbol, + TokenURI: tokenURI, + } +} + +func (msg SetURIMsg) ValidateBasic() sdk.Error { + if msg.From == nil || len(msg.From) == 0 { + return sdk.ErrInvalidAddress("sender address cannot be empty") + } + + if err := types.ValidateMiniTokenSymbol(msg.Symbol); err != nil { + return sdk.ErrInvalidCoins(err.Error()) + } + + return nil +} + +// Implements MintMsg. +func (msg SetURIMsg) Route() string { return SetURIRoute } +func (msg SetURIMsg) Type() string { return SetURIRoute } +func (msg SetURIMsg) String() string { return fmt.Sprintf("SetURI{%#v}", msg) } +func (msg SetURIMsg) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.From} } +func (msg SetURIMsg) GetSignBytes() []byte { + b, err := json.Marshal(msg) // XXX: ensure some canonical form + if err != nil { + panic(err) + } + return b +} +func (msg SetURIMsg) GetInvolvedAddresses() []sdk.AccAddress { + return msg.GetSigners() +} diff --git a/plugins/tokens/store/mapper.go b/plugins/tokens/store/mapper.go index d1c27ffcd..932184cb0 100644 --- a/plugins/tokens/store/mapper.go +++ b/plugins/tokens/store/mapper.go @@ -1,6 +1,7 @@ package store import ( + "bytes" "errors" "fmt" "strings" @@ -15,6 +16,10 @@ import ( ) type Tokens []types.Token +type MiniTokens []types.MiniToken +type ITokens []types.IToken + +const miniTokenKeyPrefix = "mini:" func (t Tokens) GetSymbols() *[]string { var symbols []string @@ -25,13 +30,15 @@ func (t Tokens) GetSymbols() *[]string { } type Mapper interface { - NewToken(ctx sdk.Context, token types.Token) error - Exists(ctx sdk.Context, symbol string) bool + NewToken(ctx sdk.Context, token types.IToken) error + ExistsBEP2(ctx sdk.Context, symbol string) bool + ExistsMini(ctx sdk.Context, symbol string) bool ExistsCC(ctx context.CLIContext, symbol string) bool - GetTokenList(ctx sdk.Context, showZeroSupplyTokens bool) Tokens - GetToken(ctx sdk.Context, symbol string) (types.Token, error) + GetTokenList(ctx sdk.Context, showZeroSupplyTokens bool, isMini bool) ITokens + GetToken(ctx sdk.Context, symbol string) (types.IToken, error) // we do not provide the updateToken method UpdateTotalSupply(ctx sdk.Context, symbol string, supply int64) error + UpdateMiniTokenURI(ctx sdk.Context, symbol string, uri string) error } var _ Mapper = mapper{} @@ -48,38 +55,35 @@ func NewMapper(cdc *wire.Codec, key sdk.StoreKey) mapper { } } -func (m mapper) GetToken(ctx sdk.Context, symbol string) (types.Token, error) { +func (m mapper) GetToken(ctx sdk.Context, symbol string) (types.IToken, error) { store := ctx.KVStore(m.key) - key := []byte(strings.ToUpper(symbol)) + var key []byte + if types.IsMiniTokenSymbol(symbol) { + key = m.calcMiniTokenKey(strings.ToUpper(symbol)) + } else { + key = []byte(strings.ToUpper(symbol)) + } bz := store.Get(key) if bz != nil { return m.decodeToken(bz), nil } - return types.Token{}, fmt.Errorf("token(%v) not found", symbol) -} - -func (m mapper) GetTokenCC(ctx context.CLIContext, symbol string) (types.Token, error) { - key := []byte(strings.ToUpper(symbol)) - bz, err := ctx.QueryStore(key, common.TokenStoreName) - if err != nil { - return types.Token{}, err - } - if bz != nil { - return m.decodeToken(bz), nil - } - return types.Token{}, fmt.Errorf("token(%v) not found", symbol) + return nil, fmt.Errorf("token(%v) not found", symbol) } -func (m mapper) GetTokenList(ctx sdk.Context, showZeroSupplyTokens bool) Tokens { - var res Tokens +func (m mapper) GetTokenList(ctx sdk.Context, showZeroSupplyTokens bool, isMini bool) ITokens { + var res ITokens store := ctx.KVStore(m.key) iter := store.Iterator(nil, nil) defer iter.Close() for ; iter.Valid(); iter.Next() { + isValid := isMini == bytes.HasPrefix(iter.Key(), []byte(miniTokenKeyPrefix)) + if !isValid { + continue + } token := m.decodeToken(iter.Value()) - if !showZeroSupplyTokens && token.TotalSupply.ToInt64() == 0 { + if !showZeroSupplyTokens && token.GetTotalSupply().ToInt64() == 0 { continue } res = append(res, token) @@ -87,14 +91,32 @@ func (m mapper) GetTokenList(ctx sdk.Context, showZeroSupplyTokens bool) Tokens return res } -func (m mapper) Exists(ctx sdk.Context, symbol string) bool { +func (m mapper) ExistsBEP2(ctx sdk.Context, symbol string) bool { + return m.exists(ctx, symbol, false) +} + +func (m mapper) ExistsMini(ctx sdk.Context, symbol string) bool { + return m.exists(ctx, symbol, true) +} + +func (m mapper) exists(ctx sdk.Context, symbol string, isMini bool) bool { store := ctx.KVStore(m.key) - key := []byte(strings.ToUpper(symbol)) + var key []byte + if isMini { + key = m.calcMiniTokenKey(strings.ToUpper(symbol)) + } else { + key = []byte(strings.ToUpper(symbol)) + } return store.Has(key) } func (m mapper) ExistsCC(ctx context.CLIContext, symbol string) bool { - key := []byte(strings.ToUpper(symbol)) + var key []byte + if types.IsMiniTokenSymbol(symbol) { + key = m.calcMiniTokenKey(strings.ToUpper(symbol)) + } else { + key = []byte(strings.ToUpper(symbol)) + } bz, err := ctx.QueryStore(key, common.TokenStoreName) if err != nil { return false @@ -105,12 +127,21 @@ func (m mapper) ExistsCC(ctx context.CLIContext, symbol string) bool { return false } -func (m mapper) NewToken(ctx sdk.Context, token types.Token) error { - symbol := token.Symbol - if err := types.ValidateToken(token); err != nil { - return err +func (m mapper) NewToken(ctx sdk.Context, token types.IToken) error { + symbol := token.GetSymbol() + var key []byte + if types.IsMiniTokenSymbol(symbol) { + key = m.calcMiniTokenKey(strings.ToUpper(symbol)) + } else { + if err := types.ValidateTokenSymbol(token.GetSymbol()); err != nil { + return err + } + if err := types.ValidateIssueSymbol(token.GetOrigSymbol()); err != nil { + return err + } + key = []byte(strings.ToUpper(symbol)) } - key := []byte(strings.ToUpper(symbol)) + store := ctx.KVStore(m.key) value := m.encodeToken(token) store.Set(key, value) @@ -121,8 +152,12 @@ func (m mapper) UpdateTotalSupply(ctx sdk.Context, symbol string, supply int64) if len(symbol) == 0 { return errors.New("symbol cannot be empty") } - - key := []byte(strings.ToUpper(symbol)) + var key []byte + if types.IsMiniTokenSymbol(symbol) { + key = m.calcMiniTokenKey(strings.ToUpper(symbol)) + } else { + key = []byte(strings.ToUpper(symbol)) + } store := ctx.KVStore(m.key) bz := store.Get(key) if bz == nil { @@ -131,14 +166,14 @@ func (m mapper) UpdateTotalSupply(ctx sdk.Context, symbol string, supply int64) toBeUpdated := m.decodeToken(bz) - if toBeUpdated.TotalSupply.ToInt64() != supply { - toBeUpdated.TotalSupply = utils.Fixed8(supply) + if toBeUpdated.GetTotalSupply().ToInt64() != supply { + toBeUpdated.SetTotalSupply(utils.Fixed8(supply)) store.Set(key, m.encodeToken(toBeUpdated)) } return nil } -func (m mapper) encodeToken(token types.Token) []byte { +func (m mapper) encodeToken(token types.IToken) []byte { bz, err := m.cdc.MarshalBinaryBare(token) if err != nil { panic(err) @@ -146,10 +181,11 @@ func (m mapper) encodeToken(token types.Token) []byte { return bz } -func (m mapper) decodeToken(bz []byte) (token types.Token) { +func (m mapper) decodeToken(bz []byte) types.IToken { + var token types.IToken err := m.cdc.UnmarshalBinaryBare(bz, &token) if err != nil { panic(err) } - return + return token } diff --git a/plugins/tokens/store/mapper_mini.go b/plugins/tokens/store/mapper_mini.go new file mode 100644 index 000000000..58a6dfe93 --- /dev/null +++ b/plugins/tokens/store/mapper_mini.go @@ -0,0 +1,52 @@ +package store + +import ( + "bytes" + "errors" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/binance-chain/node/common/types" +) + +func (m mapper) UpdateMiniTokenURI(ctx sdk.Context, symbol string, uri string) error { + if len(symbol) == 0 { + return errors.New("symbol cannot be empty") + } + + if len(uri) == 0 { + return errors.New("uri cannot be empty") + } + + if len(uri) > 2048 { + return errors.New("uri length cannot be larger than 2048") + } + + key := m.calcMiniTokenKey(strings.ToUpper(symbol)) + store := ctx.KVStore(m.key) + bz := store.Get(key) + if bz == nil { + return errors.New("token does not exist") + } + + decodedToken := m.decodeToken(bz) + + toBeUpdated, ok := decodedToken.(*types.MiniToken) + if !ok { + return errors.New("token cannot be converted to MiniToken") + } + + if toBeUpdated.TokenURI != uri { + toBeUpdated.TokenURI = uri + store.Set(key, m.encodeToken(toBeUpdated)) + } + return nil +} + +func (m mapper) calcMiniTokenKey(symbol string) []byte { + var buf bytes.Buffer + buf.WriteString(miniTokenKeyPrefix) + buf.WriteString(symbol) + return buf.Bytes() +} diff --git a/plugins/tokens/swap/handler_test.go b/plugins/tokens/swap/handler_test.go index ff23198cc..e40159b3e 100644 --- a/plugins/tokens/swap/handler_test.go +++ b/plugins/tokens/swap/handler_test.go @@ -145,7 +145,7 @@ func TestHandleCreateAndClaimSwapForSingleChain(t *testing.T) { _, acc2 := testutils.NewAccount(ctx, accKeeper, 10000e8) acc2Coins := acc2.GetCoins() - acc2Coins = acc2Coins.Plus(sdk.Coins{sdk.Coin{"ABC", 1000000000000}}) + acc2Coins = acc2Coins.Plus(sdk.Coins{sdk.Coin{"ABC-123", 1000000000000}}) _ = acc2.SetCoins(acc2Coins) accKeeper.SetAccount(ctx, acc2) @@ -169,7 +169,7 @@ func TestHandleCreateAndClaimSwapForSingleChain(t *testing.T) { swapIDStr := strings.Replace(result.Log, "swapID: ", "", -1) swapID, _ := hex.DecodeString(swapIDStr) - amountABC := sdk.Coins{sdk.Coin{"ABC", 100000000}} + amountABC := sdk.Coins{sdk.Coin{"ABC-123", 100000000}} expectedIncome = "10000:BNB" msg = NewDepositHTLTMsg(acc2.GetAddress(), amountABC, swapID) @@ -205,12 +205,12 @@ func TestHandleCreateAndClaimSwapForSingleChain(t *testing.T) { require.Equal(t, Completed, swap.Status) acc1Acc := accKeeper.GetAccount(ctx, acc1.GetAddress()) - require.Equal(t, amountABC[0].Amount, acc1Acc.GetCoins().AmountOf("ABC")) + require.Equal(t, amountABC[0].Amount, acc1Acc.GetCoins().AmountOf("ABC-123")) require.Equal(t, amountBNB[0].Amount, acc1OrignalCoins.AmountOf("BNB")-acc1Acc.GetCoins().AmountOf("BNB")) acc2Acc := accKeeper.GetAccount(ctx, acc2.GetAddress()) require.Equal(t, amountBNB[0].Amount, acc2Acc.GetCoins().AmountOf("BNB")-acc2OrignalCoins.AmountOf("BNB")) - require.Equal(t, amountABC[0].Amount, acc2OrignalCoins.AmountOf("ABC")-acc2Acc.GetCoins().AmountOf("ABC")) + require.Equal(t, amountABC[0].Amount, acc2OrignalCoins.AmountOf("ABC-123")-acc2Acc.GetCoins().AmountOf("ABC-123")) } func TestHandleCreateAndRefundSwapForSingleChain(t *testing.T) { @@ -222,7 +222,7 @@ func TestHandleCreateAndRefundSwapForSingleChain(t *testing.T) { _, acc2 := testutils.NewAccount(ctx, accKeeper, 10000e8) acc2Coins := acc2.GetCoins() - acc2Coins = acc2Coins.Plus(sdk.Coins{sdk.Coin{"ABC", 1000000000000}}) + acc2Coins = acc2Coins.Plus(sdk.Coins{sdk.Coin{"ABC-123", 1000000000000}}) _ = acc2.SetCoins(acc2Coins) accKeeper.SetAccount(ctx, acc2) @@ -246,7 +246,7 @@ func TestHandleCreateAndRefundSwapForSingleChain(t *testing.T) { swapIDStr := strings.Replace(result.Log, "swapID: ", "", -1) swapID, _ := hex.DecodeString(swapIDStr) - amountABC := sdk.Coins{sdk.Coin{"ABC", 100000000}} + amountABC := sdk.Coins{sdk.Coin{"ABC-123", 100000000}} expectedIncome = "10000:BNB" msg = NewDepositHTLTMsg(acc2.GetAddress(), amountABC, swapID) diff --git a/plugins/tokens/swap/msg.go b/plugins/tokens/swap/msg.go index d1706c96a..2fee4581e 100644 --- a/plugins/tokens/swap/msg.go +++ b/plugins/tokens/swap/msg.go @@ -5,6 +5,9 @@ import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/binance-chain/node/common/types" + "github.com/binance-chain/node/common/upgrade" ) const ( @@ -99,6 +102,13 @@ func (msg HTLTMsg) ValidateBasic() sdk.Error { if msg.HeightSpan < MinimumHeightSpan || msg.HeightSpan > MaximumHeightSpan { return ErrInvalidHeightSpan("The height span should be no less than 360 and no greater than 518400") } + + if sdk.IsUpgrade(upgrade.BEP8) { + symbolError := types.ValidateTokenSymbols(msg.Amount) + if symbolError != nil { + return sdk.ErrInvalidCoins(symbolError.Error()) + } + } return nil } @@ -148,6 +158,12 @@ func (msg DepositHTLTMsg) ValidateBasic() sdk.Error { if !msg.Amount.IsPositive() { return sdk.ErrInvalidCoins("The swapped out coins must be positive") } + if sdk.IsUpgrade(upgrade.BEP8) { + symbolError := types.ValidateTokenSymbols(msg.Amount) + if symbolError != nil { + return sdk.ErrInvalidCoins(symbolError.Error()) + } + } return nil } diff --git a/plugins/tokens/timelock/msgs.go b/plugins/tokens/timelock/msgs.go index 46fc46f8f..5a57e15e8 100644 --- a/plugins/tokens/timelock/msgs.go +++ b/plugins/tokens/timelock/msgs.go @@ -6,6 +6,8 @@ import ( "time" sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/binance-chain/node/common/types" ) const ( @@ -67,6 +69,13 @@ func (msg TimeLockMsg) ValidateBasic() sdk.Error { return sdk.ErrInvalidCoins(msg.Amount.String()) } + if sdk.IsUpgrade(sdk.BEP8) { + symbolError := types.ValidateTokenSymbols(msg.Amount) + if symbolError != nil { + return sdk.ErrInvalidCoins(symbolError.Error()) + } + } + return nil } @@ -141,6 +150,12 @@ func (msg TimeRelockMsg) ValidateBasic() sdk.Error { return ErrInvalidRelock(DefaultCodespace, fmt.Sprintf("nothing to update for time lock")) } + if sdk.IsUpgrade(sdk.BEP8) { + symbolError := types.ValidateTokenSymbols(msg.Amount) + if symbolError != nil { + return sdk.ErrInvalidCoins(symbolError.Error()) + } + } return nil } diff --git a/plugins/tokens/tokens.go b/plugins/tokens/tokens.go index 29e93afe3..7a9547342 100644 --- a/plugins/tokens/tokens.go +++ b/plugins/tokens/tokens.go @@ -3,3 +3,4 @@ package tokens import "github.com/binance-chain/node/plugins/tokens/store" type Mapper = store.Mapper +var NewMapper = store.NewMapper diff --git a/plugins/tokens/wire.go b/plugins/tokens/wire.go index af0df4740..f4ae7a1ff 100644 --- a/plugins/tokens/wire.go +++ b/plugins/tokens/wire.go @@ -4,6 +4,7 @@ import ( "github.com/binance-chain/node/plugins/tokens/burn" "github.com/binance-chain/node/plugins/tokens/freeze" "github.com/binance-chain/node/plugins/tokens/issue" + "github.com/binance-chain/node/plugins/tokens/seturi" "github.com/binance-chain/node/plugins/tokens/swap" "github.com/binance-chain/node/plugins/tokens/timelock" "github.com/binance-chain/node/wire" @@ -23,4 +24,7 @@ func RegisterWire(cdc *wire.Codec) { cdc.RegisterConcrete(swap.DepositHTLTMsg{}, "tokens/DepositHTLTMsg", nil) cdc.RegisterConcrete(swap.ClaimHTLTMsg{}, "tokens/ClaimHTLTMsg", nil) cdc.RegisterConcrete(swap.RefundHTLTMsg{}, "tokens/RefundHTLTMsg", nil) + cdc.RegisterConcrete(issue.IssueMiniMsg{}, "tokens/IssueMiniMsg", nil) + cdc.RegisterConcrete(issue.IssueTinyMsg{}, "tokens/IssueTinyMsg", nil) + cdc.RegisterConcrete(seturi.SetURIMsg{}, "tokens/SetURIMsg", nil) }