diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 047dc5a..2e83049 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -15,7 +15,7 @@ on: jobs: check: # The type of runner that the job will run on - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 diff --git a/CHANGELOG.md b/CHANGELOG.md index fc88d62..18927a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# v0.9.30-mainnet.203 + +- enable abyssworld betting + # v0.9.30-mainnet.202 - hot fix tournament diff --git a/Cargo.lock b/Cargo.lock index ffc44e6..01f14f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4994,6 +4994,35 @@ dependencies = [ "libm 0.1.4", ] +[[package]] +name = "pallet-abyss-mall" +version = "4.0.2" +dependencies = [ + "ascii", + "base64 0.13.1", + "chrono", + "frame-benchmarking", + "frame-support", + "frame-system", + "fuso-support", + "hex", + "log", + "pallet-balances", + "pallet-chainbridge", + "pallet-fuso-indicator", + "pallet-fuso-token", + "pallet-fuso-verifier", + "pallet-timestamp", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-keyring", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-abyss-tournament" version = "4.0.2" diff --git a/Cargo.toml b/Cargo.toml index 1990f39..5d8ec89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,5 +22,6 @@ members = [ 'pallets/bot', 'pallets/chainbridge', 'pallets/chainbridge-handler', - 'pallets/abyss-tournament' + 'pallets/abyss-tournament', + 'pallets/abyss-mall', ] diff --git a/pallets/abyss-mall/Cargo.toml b/pallets/abyss-mall/Cargo.toml new file mode 100644 index 0000000..f568e93 --- /dev/null +++ b/pallets/abyss-mall/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "pallet-abyss-mall" +version = "4.0.2" +authors = ["UINB Tech"] +edition = "2021" +license = "Apache-2.0" +homepage = "https://www.fusotao.org" +repository = "https://github.com/uinb/fusotao" +description = "abyssworld mall pallet" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { default-features = false, features = ['derive'], package = 'parity-scale-codec', version = '3.0.0' } +scale-info = { version = "2.0.1", default-features = false, features = ["derive"] } +frame-system = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.30" } +frame-support = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.30" } +frame-benchmarking = { default-features = false, optional = true, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.30"} +sp-std = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.30" } +ascii = { version = "1.0", default-features = false } +log = { version = "0.4.14", default-features = false } +serde = { default-features = false, version = "1.0.126" } +base64 = {version = "0.13.1", default-features = false } +chrono = {version = "0.4.26", default-features = false } +sp-core = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.30" } +sp-io = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.30" } +pallet-timestamp = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.30" } +sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.30" } +pallet-balances = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.30" } +fuso-support = { path = "../fuso-support", default-features = false } +pallet-chainbridge = { path = "../chainbridge", default-features = false } + +[dev-dependencies] +sp-keyring = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.30" } +pallet-balances = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.30" } +pallet-fuso-verifier = {path = "../verifier", default-features = false } +pallet-fuso-token = {path = "../token", default-features = false } +pallet-fuso-indicator = {path = "../indicator", default-features = false } +hex = "0.4.3" +[features] +default = ['std'] +std = [ + 'codec/std', + 'frame-support/std', + 'frame-system/std', + 'frame-benchmarking/std', + 'sp-std/std', + 'sp-core/std', + 'sp-io/std', + 'sp-runtime/std', + 'pallet-fuso-token/std', + 'pallet-fuso-verifier/std', + 'chrono/std' +] +runtime-benchmarks = [ + 'frame-benchmarking', + 'frame-support/runtime-benchmarks', + 'frame-system/runtime-benchmarks', +] diff --git a/pallets/abyss-mall/src/lib.rs b/pallets/abyss-mall/src/lib.rs new file mode 100644 index 0000000..a3b387c --- /dev/null +++ b/pallets/abyss-mall/src/lib.rs @@ -0,0 +1,83 @@ +// Copyright 2021-2023 UINB Technologies Pte. Ltd. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg_attr(not(feature = "std"), no_std)] +pub use pallet::*; + +#[frame_support::pallet] +pub mod pallet { + use ascii::AsciiStr; + use chrono::NaiveDateTime; + use frame_support::traits::fungibles::Mutate; + use frame_support::traits::{tokens::BalanceConversion, Time}; + use frame_support::{pallet_prelude::*, transactional}; + use frame_system::pallet_prelude::*; + use fuso_support::chainbridge::*; + use fuso_support::traits::{DecimalsTransformer, PriceOracle, ReservableToken, Token}; + use pallet_chainbridge as bridge; + use sp_core::bounded::BoundedBTreeMap; + use sp_runtime::traits::{TrailingZeroInput, Zero}; + use sp_runtime::Perquintill; + use sp_std::vec; + use sp_std::vec::Vec; + + type BalanceOf = <::Fungibles as Token< + ::AccountId, + >>::Balance; + + type AssetId = <::Fungibles as Token< + ::AccountId, + >>::TokenId; + + #[pallet::config] + pub trait Config: frame_system::Config + bridge::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + type Assets: ReservableToken; + + type BalanceConversion: BalanceConversion, AssetId, BalanceOf>; + + #[pallet::constant] + type AwtTokenId: Get>; + } + + #[pallet::event] + #[pallet::generate_deposit(pub (super) fn deposit_event)] + pub enum Event {} + + #[pallet::error] + pub enum Error {} + + #[pallet::pallet] + #[pallet::without_storage_info] + #[pallet::generate_store(pub (super) trait Store)] + pub struct Pallet(_); + + #[pallet::hooks] + impl Hooks> for Pallet {} + + #[pallet::call] + impl Pallet { + #[transactional] + #[pallet::weight(195_000_000)] + pub fn stake_awt( + origin: OriginFor, + awt_amount: BalanceOf, + days: u16, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + Ok(().into()) + } + } +} diff --git a/pallets/abyss-tournament/src/betting_tests.rs b/pallets/abyss-tournament/src/betting_tests.rs new file mode 100644 index 0000000..9dca006 --- /dev/null +++ b/pallets/abyss-tournament/src/betting_tests.rs @@ -0,0 +1,628 @@ +#![cfg(test)] + +use crate::mock::*; +use crate::{BattleType, Betting, BettingType, Error, OddsItem, Pallet, NPC}; +use frame_support::{assert_noop, assert_ok}; +use fuso_support::XToken; +use pallet_fuso_token::TokenAccountData; +use sp_keyring::AccountKeyring; +use sp_runtime::traits::Zero; + +type Tournament = Pallet; + +#[test] +fn test_all() { + new_test_ext().execute_with(|| { + init(); + create_betting(); + drop_betting(); + do_bet(); + set_result(); + claim(); + }); +} + +pub fn claim() { + let alice: AccountId = AccountKeyring::Alice.into(); + + assert_eq!( + pallet_fuso_token::Pallet::::get_token_balance((&1u32, &alice)), + TokenAccountData { + free: 9940_000_000_000_000_000_000, + reserved: Zero::zero(), + } + ); + + assert_eq!( + Tournament::get_betting_records_info((&alice, 1)), + (vec![(1, 200, 20_000_000_000_000_000_000)], false) + ); + + assert_eq!( + Tournament::get_betting_records_info((&alice, 2)), + ( + vec![ + (0, 600, 20000000000000000000), + (0, 400, 20000000000000000000) + ], + false + ) + ); + assert_ok!(Tournament::betting_claim( + RuntimeOrigin::signed(alice.clone()), + 1 + )); + + assert_noop!( + Tournament::betting_claim(RuntimeOrigin::signed(alice.clone()), 1), + Error::::HaveNoBonus + ); + + assert_ok!(Tournament::betting_claim( + RuntimeOrigin::signed(alice.clone()), + 2 + )); + + assert_noop!( + Tournament::betting_claim(RuntimeOrigin::signed(alice.clone()), 2), + Error::::HaveNoBonus + ); + + assert_eq!( + pallet_fuso_token::Pallet::::get_token_balance((&1u32, &alice)), + TokenAccountData { + free: 10140_000_000_000_000_000_000, + reserved: Zero::zero(), + } + ); + + assert_eq!( + Tournament::get_betting_records_info((&alice, 1)), + (vec![(1, 200, 20_000_000_000_000_000_000)], true) + ); + + assert_eq!( + Tournament::get_betting_records_info((&alice, 2)), + ( + vec![ + (0, 600, 20000000000000000000), + (0, 400, 20000000000000000000) + ], + true + ) + ); + + assert_eq!( + pallet_fuso_token::Pallet::::get_token_balance(( + &1u32, + &Tournament::get_betting_treasury(1) + )), + TokenAccountData { + free: 100_000_000_000_000_000_000, + reserved: Zero::zero(), + } + ); + + assert_eq!( + pallet_fuso_token::Pallet::::get_token_balance(( + &1u32, + &Tournament::get_betting_treasury(2) + )), + TokenAccountData { + free: 100_000_000_000_000_000_000, + reserved: Zero::zero(), + } + ); + assert_eq!( + pallet_fuso_token::Pallet::::get_token_balance((&1u32, &TREASURY)), + TokenAccountData { + free: 9660000000000000000000, + reserved: Zero::zero(), + } + ); + assert_ok!(Tournament::revoke_remain_compensate( + RuntimeOrigin::signed(TREASURY), + 1 + )); + + assert_eq!( + pallet_fuso_token::Pallet::::get_token_balance((&1u32, &TREASURY)), + TokenAccountData { + free: 9760000000000000000000, + reserved: Zero::zero(), + } + ); + assert_ok!(Tournament::revoke_remain_compensate( + RuntimeOrigin::signed(TREASURY), + 2 + )); + + assert_eq!( + pallet_fuso_token::Pallet::::get_token_balance((&1u32, &TREASURY)), + TokenAccountData { + free: 9860000000000000000000, + reserved: Zero::zero(), + } + ); +} + +pub fn set_result() { + assert_ok!(Tournament::set_result( + RuntimeOrigin::signed(TREASURY), + 1u32, + 3u8, + 0u8, + "11".to_string().into_bytes() + )); + assert_ok!(Tournament::league_settle( + RuntimeOrigin::signed(TREASURY), + 1u32, + )); +} + +pub fn do_bet() { + let alice: AccountId = AccountKeyring::Alice.into(); + assert_noop!( + Tournament::go_bet( + RuntimeOrigin::signed(alice.clone()), + 1u32, + 1, + 10_000_000_000_000_000_000 + ), + Error::::BettingAmountTooSmall + ); + assert_noop!( + Tournament::go_bet( + RuntimeOrigin::signed(alice.clone()), + 1u32, + 1, + 100_000_000_000_000_000_000 + ), + Error::::BettingAmountOverflow + ); + assert_noop!( + Tournament::go_bet( + RuntimeOrigin::signed(alice.clone()), + 1u32, + 3, + 20_000_000_000_000_000_000 + ), + Error::::SelectIndexOverflow + ); + + assert_ok!(Tournament::go_bet( + RuntimeOrigin::signed(alice.clone()), + 1u32, + 1, + 20_000_000_000_000_000_000 + )); + + assert_eq!( + Tournament::get_betting_info(&1), + Some(Betting { + creator: TREASURY, + pledge_account: Tournament::get_betting_treasury(1), + total_pledge: 100_000_000_000_000_000_000, + betting_type: BettingType::WinLose, + battles: vec![1], + odds: vec![ + OddsItem { + win_lose: vec![1], + score: vec![], + o: 200, + total_compensate_amount: 0, + buy_in: 0, + accounts: 0 + }, + OddsItem { + win_lose: vec![2], + score: vec![], + o: 200, + total_compensate_amount: 40_000_000_000_000_000_000, + buy_in: 20_000_000_000_000_000_000, + accounts: 1 + } + ], + token_id: 1u32, + min_betting_amount: 20000000000000000000, + season: 1u32 + }) + ); + + assert_noop!( + Tournament::go_bet( + RuntimeOrigin::signed(alice.clone()), + 2u32, + 0, + 10_000_000_000_000_000_000 + ), + Error::::BettingAmountTooSmall + ); + assert_noop!( + Tournament::go_bet( + RuntimeOrigin::signed(alice.clone()), + 2u32, + 0, + 100_000_000_000_000_000_000 + ), + Error::::BettingAmountOverflow + ); + assert_noop!( + Tournament::go_bet( + RuntimeOrigin::signed(alice.clone()), + 2u32, + 6, + 20_000_000_000_000_000_000 + ), + Error::::SelectIndexOverflow + ); + assert_ok!(Tournament::go_bet( + RuntimeOrigin::signed(alice.clone()), + 2u32, + 0, + 20_000_000_000_000_000_000 + )); + assert_ok!(Tournament::update_odds( + RuntimeOrigin::signed(TREASURY), + 2u32, + vec![(0u16, 400u16)] + )); + assert_ok!(Tournament::go_bet( + RuntimeOrigin::signed(alice.clone()), + 2u32, + 0, + 20_000_000_000_000_000_000 + )); + assert_noop!( + Tournament::go_bet( + RuntimeOrigin::signed(alice.clone()), + 2u32, + 0, + 40_000_000_000_000_000_000 + ), + Error::::BettingAmountOverflow + ); + assert_noop!( + Tournament::go_bet( + RuntimeOrigin::signed(alice.clone()), + 5u32, + 0, + 20_000_000_000_000_000_000 + ), + Error::::BettingNotFound + ); + + assert_eq!( + Tournament::get_betting_info(&2), + Some(Betting { + creator: TREASURY, + pledge_account: Tournament::get_betting_treasury(2), + total_pledge: 300_000_000_000_000_000_000, + betting_type: BettingType::Score, + battles: vec![1], + odds: vec![ + OddsItem { + win_lose: vec![], + score: vec![(3, 0)], + o: 400, + total_compensate_amount: 200_000_000_000_000_000_000, + buy_in: 40_000_000_000_000_000_000, + accounts: 2 + }, + OddsItem { + win_lose: vec![], + score: vec![(3, 1)], + o: 600, + total_compensate_amount: 0, + buy_in: 0, + accounts: 0 + }, + OddsItem { + win_lose: vec![], + score: vec![(3, 2)], + o: 600, + total_compensate_amount: 0, + buy_in: 0, + accounts: 0 + }, + OddsItem { + win_lose: vec![], + score: vec![(0, 3)], + o: 600, + total_compensate_amount: 0, + buy_in: 0, + accounts: 0 + }, + OddsItem { + win_lose: vec![], + score: vec![(1, 3)], + o: 600, + total_compensate_amount: 0, + buy_in: 0, + accounts: 0 + }, + OddsItem { + win_lose: vec![], + score: vec![(2, 3)], + o: 600, + total_compensate_amount: 0, + buy_in: 0, + accounts: 0 + }, + ], + token_id: 1u32, + min_betting_amount: 20000000000000000000, + season: 1u32 + }) + ); +} + +pub fn init() { + assert_ok!(Tournament::create_npc( + RuntimeOrigin::signed(TREASURY), + b"npc1".to_vec(), + b"fsgrethges".to_vec(), + b"sgseirgeiwrgwerhw".to_vec(), + b"csghert".to_vec(), + )); + assert_eq!( + Tournament::get_npc_info(&1), + Some(NPC { + name: b"npc1".to_vec(), + img_url: b"fsgrethges".to_vec(), + story: b"sgseirgeiwrgwerhw".to_vec(), + features: b"csghert".to_vec(), + }) + ); + + assert_ok!(Tournament::create_npc( + RuntimeOrigin::signed(TREASURY), + b"npc2".to_vec(), + b"fsgrethges".to_vec(), + b"sgseirgeiwrgwerhw".to_vec(), + b"csghert".to_vec(), + )); + + assert_ok!(Tournament::create_season( + RuntimeOrigin::signed(TREASURY), + b"sdsd".to_vec(), + "2023-07-25 00:00:00".into(), + BattleType::QuarterFinals, + 100000000000000000000 + )); + assert_ok!(Tournament::set_default_season( + RuntimeOrigin::signed(TREASURY), + 1, + )); + + assert_ok!(Tournament::update_season_current_round( + RuntimeOrigin::signed(TREASURY), + 1, + BattleType::League + )); + assert_ok!(Tournament::create_battle( + RuntimeOrigin::signed(TREASURY), + 1, + BattleType::League, + 1, + 2, + "2023-07-30 00:00:00".into(), + 1 + )); + + let alice: AccountId = AccountKeyring::Alice.into(); + let awt_id = 1u32; + let awt = XToken::NEP141( + br#"AWT"#.to_vec(), + br#"AWT"#.to_vec(), + Zero::zero(), + false, + 2, + ); + assert_ok!(pallet_fuso_token::Pallet::::issue( + RuntimeOrigin::signed(alice.clone()), + awt, + )); + let _ = pallet_fuso_token::Pallet::::do_mint(awt_id, &alice, 1000000, None); + let _ = pallet_fuso_token::Pallet::::do_mint(awt_id, &TREASURY, 1000000, None); +} + +pub fn drop_betting() { + assert_eq!( + pallet_fuso_token::Pallet::::get_token_balance((&1u32, &TREASURY)), + TokenAccountData { + free: 9600_000_000_000_000_000_000, + reserved: Zero::zero(), + } + ); + assert_ok!(Tournament::create_betting( + RuntimeOrigin::signed(TREASURY), + BettingType::WinLose, + vec![1], + vec![], + 1, + 100_000_000_000_000_000_000 + )); + assert_eq!(Tournament::get_bettings_by_battle(1), vec![1, 2, 3]); + assert_ok!(Tournament::drop_betting(RuntimeOrigin::signed(TREASURY), 3,)); + assert_eq!(Tournament::get_bettings_by_battle(1), vec![1, 2]); + assert_eq!( + pallet_fuso_token::Pallet::::get_token_balance((&1u32, &TREASURY)), + TokenAccountData { + free: 9600_000_000_000_000_000_000, + reserved: Zero::zero(), + } + ); + + assert_eq!( + Tournament::get_betting_info(&3), + Some(Betting { + creator: TREASURY, + pledge_account: Tournament::get_betting_treasury(3), + total_pledge: 0, + betting_type: BettingType::WinLose, + battles: vec![1], + odds: vec![ + OddsItem { + win_lose: vec![1], + score: vec![], + o: 200, + total_compensate_amount: 0, + buy_in: 0, + accounts: 0 + }, + OddsItem { + win_lose: vec![2], + score: vec![], + o: 200, + total_compensate_amount: 0, + buy_in: 0, + accounts: 0 + } + ], + token_id: 1u32, + min_betting_amount: 20000000000000000000, + season: 1u32 + }) + ); +} + +pub fn create_betting() { + assert_ok!(Tournament::create_betting( + RuntimeOrigin::signed(TREASURY), + BettingType::WinLose, + vec![1], + vec![], + 1, + 100_000_000_000_000_000_000 + )); + + assert_eq!( + Tournament::get_betting_info(&1), + Some(Betting { + creator: TREASURY, + pledge_account: Tournament::get_betting_treasury(1), + total_pledge: 100_000_000_000_000_000_000, + betting_type: BettingType::WinLose, + battles: vec![1], + odds: vec![ + OddsItem { + win_lose: vec![1], + score: vec![], + o: 200, + total_compensate_amount: 0, + buy_in: 0, + accounts: 0 + }, + OddsItem { + win_lose: vec![2], + score: vec![], + o: 200, + total_compensate_amount: 0, + buy_in: 0, + accounts: 0 + } + ], + token_id: 1u32, + min_betting_amount: 20000000000000000000, + season: 1u32 + }) + ); + assert_noop!( + Tournament::create_betting( + RuntimeOrigin::signed(TREASURY), + BettingType::Score, + vec![1], + vec![], + 1, + 10000_000_000_000_000_000_000 + ), + pallet_fuso_token::Error::::InsufficientBalance + ); + assert_eq!( + pallet_fuso_token::Pallet::::get_token_balance((&1u32, &TREASURY)), + TokenAccountData { + free: 9900_000_000_000_000_000_000, + reserved: Zero::zero(), + } + ); + assert_ok!(Tournament::create_betting( + RuntimeOrigin::signed(TREASURY), + BettingType::Score, + vec![1], + vec![], + 1, + 300_000_000_000_000_000_000 + )); + assert_eq!( + Tournament::get_betting_info(&2), + Some(Betting { + creator: TREASURY, + pledge_account: Tournament::get_betting_treasury(2), + total_pledge: 300_000_000_000_000_000_000, + betting_type: BettingType::Score, + battles: vec![1], + odds: vec![ + OddsItem { + win_lose: vec![], + score: vec![(3, 0)], + o: 600, + total_compensate_amount: 0, + buy_in: 0, + accounts: 0 + }, + OddsItem { + win_lose: vec![], + score: vec![(3, 1)], + o: 600, + total_compensate_amount: 0, + buy_in: 0, + accounts: 0 + }, + OddsItem { + win_lose: vec![], + score: vec![(3, 2)], + o: 600, + total_compensate_amount: 0, + buy_in: 0, + accounts: 0 + }, + OddsItem { + win_lose: vec![], + score: vec![(0, 3)], + o: 600, + total_compensate_amount: 0, + buy_in: 0, + accounts: 0 + }, + OddsItem { + win_lose: vec![], + score: vec![(1, 3)], + o: 600, + total_compensate_amount: 0, + buy_in: 0, + accounts: 0 + }, + OddsItem { + win_lose: vec![], + score: vec![(2, 3)], + o: 600, + total_compensate_amount: 0, + buy_in: 0, + accounts: 0 + }, + ], + token_id: 1u32, + min_betting_amount: 20000000000000000000, + season: 1u32 + }) + ); + assert_eq!( + pallet_fuso_token::Pallet::::get_token_balance((&1u32, &TREASURY)), + TokenAccountData { + free: 9600_000_000_000_000_000_000, + reserved: Zero::zero(), + } + ); + assert_eq!(Tournament::get_bettings_by_battle(1), vec![1, 2]); +} diff --git a/pallets/abyss-tournament/src/lib.rs b/pallets/abyss-tournament/src/lib.rs index 3bb608a..45cc792 100644 --- a/pallets/abyss-tournament/src/lib.rs +++ b/pallets/abyss-tournament/src/lib.rs @@ -15,6 +15,8 @@ #![cfg_attr(not(feature = "std"), no_std)] pub use pallet::*; +#[cfg(test)] +pub mod betting_tests; #[cfg(test)] pub mod mock; #[cfg(test)] @@ -29,12 +31,16 @@ pub mod pallet { use frame_support::{pallet_prelude::*, transactional}; use frame_system::pallet_prelude::*; use fuso_support::chainbridge::*; - use fuso_support::traits::{ReservableToken, Token}; + use fuso_support::traits::{DecimalsTransformer, PriceOracle, ReservableToken, Token}; use pallet_chainbridge as bridge; use sp_core::bounded::BoundedBTreeMap; use sp_runtime::traits::{TrailingZeroInput, Zero}; + use sp_runtime::Perquintill; + use sp_std::vec; use sp_std::vec::Vec; + const QUINTILL: u128 = 1_000_000_000_000_000_000; + #[derive(Encode, Decode, Clone, PartialEq, Eq, Default, TypeInfo, Debug)] pub struct NPC { pub name: Vec, @@ -70,6 +76,27 @@ pub mod pallet { pub video_url: Vec, } + impl Battle { + fn calc(&self) -> (u8, u32, u32) { + let mut score_diff: Score = 0; + let mut winner: NpcId = 0; + let mut loser: NpcId = 0; + match self.home_score > self.visiting_score { + true => { + score_diff = self.home_score.unwrap() - self.visiting_score.unwrap(); + winner = self.home; + loser = self.visiting; + } + false => { + score_diff = self.visiting_score.unwrap() - self.home_score.unwrap(); + winner = self.visiting; + loser = self.home; + } + }; + (score_diff, winner, loser) + } + } + #[derive(Encode, Decode, Clone, PartialEq, Eq, Default, TypeInfo, Debug)] pub struct BattleAbstract { pub season: SeasonId, @@ -94,18 +121,150 @@ pub mod pallet { } #[derive(Encode, Decode, Clone, PartialEq, Eq, Default, TypeInfo, Debug)] - pub struct Odds { - pub battle: Vec<(BattleId, NpcId)>, - pub o: u128, + pub struct OddsItem { + pub win_lose: Vec, + pub score: Vec<(Score, Score)>, + pub o: OddsNumber, + pub total_compensate_amount: Balance, + pub buy_in: Balance, + pub accounts: u32, } #[derive(Encode, Decode, Clone, PartialEq, Eq, Default, TypeInfo, Debug)] - pub struct Betting { + pub struct OddsItemPrams { + pub win_lose: Vec, + pub score: Vec<(Score, Score)>, + pub o: OddsNumber, + } + + impl Into> for OddsItemPrams { + fn into(self) -> OddsItem { + OddsItem { + win_lose: self.win_lose, + score: self.score, + o: self.o, + total_compensate_amount: Zero::zero(), + buy_in: Zero::zero(), + accounts: 0, + } + } + } + + #[derive(Encode, Decode, Clone, PartialEq, Eq, Default, TypeInfo, Debug)] + pub struct Betting { + pub creator: AccountId, + pub pledge_account: AccountId, + pub total_pledge: Balance, + pub betting_type: BettingType, pub battles: Vec, - pub odds: Vec, + pub odds: Vec>, + pub token_id: TokenId, + pub min_betting_amount: Balance, pub season: SeasonId, - pub start_time: u64, } + + impl OddsItem { + pub fn check(&self, betting_type: &BettingType, battle_size: usize) -> bool { + match betting_type { + BettingType::Score => { + if self.score.len() != battle_size { + return false; + } + for x in &self.score { + let h = x.0; + let v = x.1; + if h != 3 && v != 3 { + return false; + } + if h > 3 || v > 3 { + return false; + } + if h == v { + return false; + } + } + } + BettingType::WinLose => { + if self.win_lose.len() != battle_size { + return false; + } + for x in &self.win_lose { + if *x != 1 && *x != 2 { + return false; + } + } + } + } + if self.o <= 100 { + return false; + } + true + } + } + + impl Betting { + pub fn check(&self) -> bool { + if self.battles.len() != 1 { + return false; + } + let battle_size = self.battles.len(); + for o in &self.odds { + if o.check(&self.betting_type, battle_size) == false { + return false; + } + } + match self.betting_type { + BettingType::Score => { + let mut v = Vec::new(); + if self.odds.len() as u32 != 6u32.pow(self.battles.len() as u32) { + return false; + } + for oo in &self.odds { + if oo.score.len() != self.battles.len() { + return false; + } + v.push(oo.score.clone()); + } + + for i in 0..v.len() - 1 { + for j in (i + 1)..v.len() { + if v[i] == v[j] { + return false; + } + } + } + } + BettingType::WinLose => { + let mut v = Vec::new(); + if self.odds.len() as u32 != 2u32.pow(self.battles.len() as u32) { + return false; + } + for oo in &self.odds { + if oo.win_lose.len() != self.battles.len() { + return false; + } + v.push(oo.win_lose.clone()); + } + for i in 0..v.len() - 1 { + for j in (i + 1)..v.len() { + if v[i] == v[j] { + return false; + } + } + } + } + } + true + } + } + + #[derive(Encode, Decode, Clone, PartialEq, Eq, Default, TypeInfo, Debug)] + pub struct BettingCreateParms { + pub betting_type: BettingType, + pub battles: Vec, + pub odds: Vec>, + } + #[derive(Encode, Decode, Clone, PartialEq, Eq, Default, TypeInfo, Debug)] pub struct Season { pub id: SeasonId, @@ -116,17 +275,14 @@ pub mod pallet { pub total_battles: u8, pub bonus_strategy: Vec<(u8, u32, Balance)>, pub ticket_price: Balance, - pub first_round_battle_type: BattleType, - pub current_round_battle_type: BattleType, + pub first_finals_battle_type: BattleType, + pub current_finals_battle_type: BattleType, pub champion: Option, pub total_tickets: u32, } pub const PALLET_ID: frame_support::PalletId = frame_support::PalletId(*b"abytourn"); - pub type TokenId = - <::Assets as Token<::AccountId>>::TokenId; - type AssetId = <::Fungibles as Token< ::AccountId, >>::TokenId; @@ -143,12 +299,18 @@ pub mod pallet { type BattleId = ObjectId; + type BettingId = ObjectId; + + type OddsNumber = u16; + type SelectIndex = u16; type BattleAmount = u8; type Score = u8; + type HomeOrVisiting = u8; //1 as home,and 2 as visiting + #[pallet::config] pub trait Config: frame_system::Config + bridge::Config { type RuntimeEvent: From> + IsType<::RuntimeEvent>; @@ -169,6 +331,9 @@ pub mod pallet { #[pallet::constant] type MaxTicketAmount: Get; + #[pallet::constant] + type DefaultMinBetingAmount: Get>; + #[pallet::constant] type DonorAccount: Get; @@ -180,6 +345,14 @@ pub mod pallet { #[pallet::constant] type BvbTreasury: Get; + + #[pallet::constant] + type BvbOrganizer: Get; + + #[pallet::constant] + type SwapPoolAccount: Get; + + type Oracle: PriceOracle, BalanceOf, Self::BlockNumber>; } #[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, Debug, Default)] @@ -205,9 +378,28 @@ pub mod pallet { QuarterFinals, SemiFinals, Finals, - Regular, + League, + } + + #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, TypeInfo, Debug)] + pub enum BettingType { + #[default] + Score, + WinLose, } + impl BattleType { + fn total_battles(&self) -> u8 { + match self { + BattleType::SixteenthFinals => 31u8, + BattleType::EighthFinals => 15u8, + BattleType::QuarterFinals => 7u8, + BattleType::SemiFinals => 3u8, + BattleType::Finals => 1u8, + BattleType::League => 35u8, + } + } + } impl Into for BattleType { fn into(self) -> u8 { match self { @@ -216,7 +408,7 @@ pub mod pallet { BattleType::QuarterFinals => 4u8, BattleType::SemiFinals => 2u8, BattleType::Finals => 1u8, - BattleType::Regular => 255u8, + BattleType::League => 255u8, } } } @@ -226,7 +418,7 @@ pub mod pallet { fn try_from(value: u8) -> Result { match value { - 255 => Ok(BattleType::Regular), + 255 => Ok(BattleType::League), 16 => Ok(BattleType::SixteenthFinals), 8 => Ok(BattleType::EighthFinals), 4 => Ok(BattleType::QuarterFinals), @@ -240,7 +432,6 @@ pub mod pallet { #[pallet::event] #[pallet::generate_deposit(pub (super) fn deposit_event)] pub enum Event { - BotCreated(T::AccountId, TokenId, TokenId), NowTime(u64), NpcPoints(SeasonId, NpcId, BattleAmount, BattleAmount, u32, i32), ParticipantPoints(SeasonId, T::AccountId, BattleAmount, BattleAmount, u32), @@ -248,6 +439,7 @@ pub mod pallet { Battle(BattleId, Battle), BattleResult(BattleId, Score, Score, Vec), SeasonUpdate(Season>, bool), + BettingUpdate(BettingId, Betting, AssetId>), } #[pallet::error] @@ -276,6 +468,13 @@ pub mod pallet { SelectIndexOverflow, BattleTypeError, AddrListInputError, + BettingParamsError, + PledgeAmountZero, + BettingNotFound, + BettingAmountOverflow, + BettingError, + BettingAmountTooSmall, + PermissonDeny, } #[pallet::hooks] @@ -298,6 +497,10 @@ pub mod pallet { #[pallet::getter(fn next_battle_id)] pub type NextBattleId = StorageValue<_, ObjectId, ValueQuery, DefaultNextId>; + #[pallet::storage] + #[pallet::getter(fn next_betting_id)] + pub type NextBettingId = StorageValue<_, ObjectId, ValueQuery, DefaultNextId>; + #[pallet::storage] #[pallet::getter(fn default_season)] pub type DefaultSeason = StorageValue<_, SeasonId, ValueQuery>; @@ -306,10 +509,35 @@ pub mod pallet { #[pallet::getter(fn get_npc_info)] pub type Npcs = StorageMap<_, Twox64Concat, NpcId, NPC, OptionQuery>; + #[pallet::storage] + #[pallet::getter(fn get_bettings_by_battle)] + pub type BettingByBattle = + StorageMap<_, Twox64Concat, BattleId, Vec, ValueQuery>; + #[pallet::storage] #[pallet::getter(fn get_battle_info)] pub type Battles = StorageMap<_, Twox64Concat, BattleId, Battle, OptionQuery>; + #[pallet::storage] + #[pallet::getter(fn get_betting_info)] + pub type Bettings = StorageMap< + _, + Twox64Concat, + BettingId, + Betting, AssetId>, + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn get_betting_records_info)] + pub type BettingRecords = StorageMap< + _, + Twox64Concat, + (T::AccountId, BettingId), + (Vec<(SelectIndex, OddsNumber, BalanceOf)>, bool), + ValueQuery, + >; + #[pallet::storage] #[pallet::getter(fn get_season_winners)] pub type SeasonWinners = StorageMap< @@ -435,19 +663,18 @@ pub mod pallet { origin: OriginFor, name: Vec, start_time_str: Vec, - total_battles: BattleAmount, - first_round_battle_type: BattleType, + first_finals_battle_type: BattleType, ticket_price: BalanceOf, ) -> DispatchResultWithPostInfo { let _ = T::OrganizerOrigin::ensure_origin(origin)?; - let start_time = Self::date_to_timestamp(start_time_str) - .map_err(|_e| Error::::TimeFormatError)?; + let start_time = Self::date_to_timestamp(start_time_str)?; ensure!( ticket_price > 1000000000000000000.into(), Error::::TicketPriceTooSmall ); let id = Self::next_season_id(); let treasury = Self::get_season_treasury(id); + let total_battles = first_finals_battle_type.total_battles(); let season = Season { id, name, @@ -457,8 +684,8 @@ pub mod pallet { total_battles, bonus_strategy: Vec::default(), ticket_price, - first_round_battle_type: first_round_battle_type.clone(), - current_round_battle_type: first_round_battle_type, + first_finals_battle_type: first_finals_battle_type.clone(), + current_finals_battle_type: first_finals_battle_type, champion: None, total_tickets: 0u32, }; @@ -477,7 +704,7 @@ pub mod pallet { ) -> DispatchResultWithPostInfo { let _ = T::OrganizerOrigin::ensure_origin(origin)?; let mut s = Self::get_season_info(season_id).ok_or(Error::::SeasonNotFound)?; - s.current_round_battle_type = battle_type; + s.current_finals_battle_type = battle_type; Seasons::::insert(season_id, s.clone()); let is_default = Self::default_season() == season_id; Self::deposit_event(Event::SeasonUpdate(s, is_default)); @@ -491,13 +718,12 @@ pub mod pallet { season_id: SeasonId, name: Vec, start_time_str: Vec, - total_battles: BattleAmount, - first_round_battle_type: BattleType, + first_finals_battle_type: BattleType, + current_finals_battle_type: BattleType, ticket_price: BalanceOf, ) -> DispatchResultWithPostInfo { let _ = T::OrganizerOrigin::ensure_origin(origin)?; - let start_time = Self::date_to_timestamp(start_time_str) - .map_err(|_e| Error::::TimeFormatError)?; + let start_time = Self::date_to_timestamp(start_time_str)?; ensure!( ticket_price > 1000000000000000000.into(), Error::::TicketPriceTooSmall @@ -505,8 +731,9 @@ pub mod pallet { let mut s = Self::get_season_info(season_id).ok_or(Error::::SeasonNotFound)?; s.name = name; s.start_time = start_time; - s.total_battles = total_battles; - s.first_round_battle_type = first_round_battle_type; + s.total_battles = first_finals_battle_type.total_battles(); + s.first_finals_battle_type = first_finals_battle_type; + s.current_finals_battle_type = current_finals_battle_type; s.ticket_price = ticket_price; Seasons::::insert(season_id, s.clone()); let is_default = Self::default_season() == season_id; @@ -514,15 +741,205 @@ pub mod pallet { Ok(().into()) } + #[transactional] + #[pallet::weight(195_000_000)] + pub fn go_bet( + origin: OriginFor, + betting_id: BettingId, + item_index: SelectIndex, + amount: BalanceOf, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let betting: Betting, AssetId> = + Self::get_betting_info(betting_id).ok_or(Error::::BettingNotFound)?; + ensure!( + amount >= betting.min_betting_amount, + Error::::BettingAmountTooSmall + ); + ensure!( + usize::from(item_index) < betting.odds.len(), + Error::::SelectIndexOverflow + ); + let now = T::TimeProvider::now(); + for battle_id in betting.battles { + let battle = Self::get_battle_info(battle_id).ok_or(Error::::BattleNotFound)?; + ensure!( + now.into() / 1000 < battle.start_time, + Error::::BettingOverTime + ); + } + let select_odd = betting.odds[item_index as usize].clone(); + let current_selected_odd_value = select_odd.o; + let add_compensate_amount = + current_selected_odd_value as u128 * amount.into() / 100 as u128; + let new_compensate_amount = + select_odd.total_compensate_amount + add_compensate_amount.into(); + let pledge_amount = + T::Fungibles::free_balance(&betting.token_id, &betting.pledge_account); + ensure!( + new_compensate_amount <= pledge_amount, + Error::::BettingAmountOverflow + ); + let _ = T::Fungibles::transfer_token(&who, betting.token_id, amount, &betting.creator)?; + BettingRecords::::mutate((&who, betting_id), |r| { + let mut found = false; + for s in &mut *r.0 { + if s.0 == item_index && s.1 == current_selected_odd_value { + s.2 = s.2 + amount; + found = true; + break; + } + } + if found == false { + r.0.push((item_index, current_selected_odd_value, amount)); + } + }); + Bettings::::mutate(betting_id, |b| { + let mut betting = b.take().unwrap(); + betting.odds[item_index as usize].total_compensate_amount = new_compensate_amount; + betting.odds[item_index as usize].accounts += 1; + betting.odds[item_index as usize].buy_in += amount; + Self::deposit_event(Event::BettingUpdate(betting_id, betting.clone())); + b.replace(betting); + }); + Ok(().into()) + } + + #[transactional] + #[pallet::weight(195_000_000)] + pub fn revoke_remain_compensate( + origin: OriginFor, + betting_id: BettingId, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let betting: Betting, AssetId> = + Self::get_betting_info(betting_id).ok_or(Error::::BettingNotFound)?; + ensure!(&who == &betting.creator, Error::::PermissonDeny); + let hit_index = Self::calc_betting_hit_index(&betting)?; + let total_compensate_amount = betting.odds[hit_index as usize].total_compensate_amount; + if betting.total_pledge <= total_compensate_amount { + return Ok(().into()); + } + let _ = T::Fungibles::transfer_token( + &betting.pledge_account, + betting.token_id, + betting.total_pledge - total_compensate_amount, + &who, + )?; + Ok(().into()) + } + + #[transactional] + #[pallet::weight(195_000_000)] + pub fn betting_claim( + origin: OriginFor, + betting_id: BettingId, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let (v, b): (Vec<(SelectIndex, OddsNumber, BalanceOf)>, bool) = + Self::get_betting_records_info((&who, betting_id)); + ensure!(!b, Error::::HaveNoBonus); + ensure!(!v.is_empty(), Error::::HaveNoBonus); + let betting = Self::get_betting_info(betting_id).ok_or(Error::::BettingNotFound)?; + let hit_index = Self::calc_betting_hit_index(&betting)?; + let mut total_claim_amount: BalanceOf = 0.into(); + for s in v { + if s.0 == hit_index { + total_claim_amount += s.2 * s.1.into() / 100.into(); + } + } + if total_claim_amount > Zero::zero() { + let _ = T::Fungibles::transfer_token( + &betting.pledge_account, + betting.token_id, + total_claim_amount, + &who, + )?; + } + BettingRecords::::mutate((&who, betting_id), |r| { + r.1 = true; + }); + Ok(().into()) + } + + #[transactional] + #[pallet::weight(195_000_000)] + pub fn append_betting_pledge( + origin: OriginFor, + betting_id: BettingId, + amount: BalanceOf, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + ensure!(amount > Zero::zero(), Error::::PledgeAmountZero); + let betting: Betting, AssetId> = + Self::get_betting_info(betting_id).ok_or(Error::::BettingNotFound)?; + for battle_id in &betting.battles { + let battle = Self::get_battle_info(battle_id).ok_or(Error::::BattleNotFound)?; + ensure!( + battle.status != BattleStatus::Finalized, + Error::::BattleStatusError + ); + } + let _ = T::Fungibles::transfer_token( + &who, + betting.token_id, + amount, + &betting.pledge_account, + )?; + Bettings::::mutate(betting_id, |b| { + let mut bb = b.take().unwrap(); + bb.total_pledge = T::Fungibles::free_balance(&bb.token_id, &bb.pledge_account); + b.replace(bb); + }); + Ok(().into()) + } + #[transactional] #[pallet::weight(195_000_000)] pub fn create_betting( origin: OriginFor, - _battles: Vec, - _odds: Vec, + betting_type: BettingType, + battles: Vec, + odditem_params: Vec, + season_id: SeasonId, + pledge_amount: BalanceOf, ) -> DispatchResultWithPostInfo { let _ = T::OrganizerOrigin::ensure_origin(origin)?; - + ensure!(pledge_amount > Zero::zero(), Error::::PledgeAmountZero); + let mut odds: Vec>> = + odditem_params.iter().map(|o| o.clone().into()).collect(); + if odds.is_empty() { + odds = Self::generate_default_odd_item(&betting_type, battles.len()) + .ok_or(Error::::BettingError)?; + } + let _season = Self::get_season_info(season_id).ok_or(Error::::SeasonNotFound)?; + let betting_id: BettingId = Self::next_betting_id(); + let betting = Betting::, AssetId> { + creator: T::BvbOrganizer::get(), + pledge_account: Self::get_betting_treasury(betting_id), + total_pledge: pledge_amount, + betting_type, + battles: battles.clone(), + odds, + token_id: T::AwtTokenId::get(), + min_betting_amount: T::DefaultMinBetingAmount::get(), + season: season_id, + }; + ensure!(betting.check(), Error::::BettingParamsError); + let _ = T::Fungibles::transfer_token( + &betting.creator, + betting.token_id, + pledge_amount, + &betting.pledge_account, + )?; + Bettings::::insert(betting_id, betting.clone()); + for battleid in battles { + BettingByBattle::::mutate(battleid, |v| { + v.push(betting_id); + }) + } + NextBettingId::::mutate(|id| *id += 1); + Self::deposit_event(Event::BettingUpdate(betting_id, betting)); Ok(().into()) } @@ -596,9 +1013,11 @@ pub mod pallet { let s = Self::get_season_info(season).ok_or(Error::::SeasonNotFound)?; Self::get_npc_info(home).ok_or(Error::::NpcNotFound)?; Self::get_npc_info(visiting).ok_or(Error::::NpcNotFound)?; - let start_time = Self::date_to_timestamp(start_time_str) - .map_err(|_e| Error::::TimeFormatError)?; - ensure!(start_time >= s.start_time, Error::::BattleTimeError); + let start_time = Self::date_to_timestamp(start_time_str)?; + ensure!( + battle_type == BattleType::League || start_time >= s.start_time, + Error::::BattleTimeError + ); ensure!(home != visiting, Error::::BattleNpcCantSame); let battle = Battle { season, @@ -650,8 +1069,7 @@ pub mod pallet { let s = Self::get_season_info(season).ok_or(Error::::SeasonNotFound)?; Self::get_npc_info(home).ok_or(Error::::NpcNotFound)?; Self::get_npc_info(visiting).ok_or(Error::::NpcNotFound)?; - let start_time = Self::date_to_timestamp(start_time_str) - .map_err(|_e| Error::::TimeFormatError)?; + let start_time = Self::date_to_timestamp(start_time_str)?; ensure!(start_time >= s.start_time, Error::::BattleTimeError); ensure!(home != visiting, Error::::BattleNpcCantSame); let battle = Battle { @@ -672,6 +1090,39 @@ pub mod pallet { Ok(().into()) } + #[transactional] + #[pallet::weight(195_000_000)] + pub fn transfer_ticket( + origin: OriginFor, + season_id: SeasonId, + tickets: u32, + to: T::AccountId, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let (total_tickets, remain_tickets) = Self::get_ticket(season_id, &who); + ensure!( + total_tickets >= tickets && remain_tickets >= tickets && tickets > 0u32, + Error::::TicketAmountError + ); + Tickets::::mutate(season_id, &who, |tickets_amount| { + tickets_amount.0 = tickets_amount.0 - tickets; + tickets_amount.1 = tickets_amount.1 - tickets; + }); + Tickets::::mutate(season_id, &to, |tickets_amount| { + tickets_amount.0 = tickets_amount.0 + tickets; + tickets_amount.1 = tickets_amount.1 + tickets; + }); + if frame_system::Pallet::::account_nonce(&to) == Zero::zero() { + let _ = T::Fungibles::transfer_token( + &T::DonorAccount::get(), + T::Fungibles::native_token_id(), + T::DonationForAgent::get(), + &to, + ); + } + Ok(().into()) + } + #[pallet::weight(195_000_0000)] pub fn deposit( origin: OriginFor, @@ -697,6 +1148,65 @@ pub mod pallet { Ok(()) } + #[pallet::weight(195_000_0000)] + pub fn swap( + origin: OriginFor, + to: T::AccountId, + amt: BalanceOf, + r_id: ResourceId, + ) -> DispatchResult { + let _ = T::BridgeOrigin::ensure_origin(origin)?; + let (chain_id, _, maybe_contract) = + decode_resource_id(r_id).map_err(|_| Error::::InvalidResourceId)?; + let token_id = T::AssetIdByName::try_get_asset_id(chain_id, maybe_contract) + .map_err(|_| Error::::InvalidResourceId)?; + let who = to.clone(); + T::Fungibles::mint_into(token_id, &to, amt)?; + if frame_system::Pallet::::account_nonce(&to) == Zero::zero() { + let _ = T::Fungibles::transfer_token( + &T::DonorAccount::get(), + T::Fungibles::native_token_id(), + T::DonationForAgent::get(), + &to, + ); + } + if token_id == T::AwtTokenId::get() { + return Ok(()); + } + let stable = T::Fungibles::is_stable(&token_id); + if stable == false { + return Ok(()); + } + let external_decimals = T::Fungibles::token_external_decimals(&token_id)?; + let unified_amount = + T::Fungibles::transform_decimals_to_standard(amt, external_decimals); + let price: u128 = T::Oracle::get_price(&token_id).into(); + if price.is_zero() { + return Ok(()); + } + let awt_amount: u128 = unified_amount.into() / price * QUINTILL + + Perquintill::from_rational::(unified_amount.into() % price, price) + .deconstruct() as u128; + if T::Fungibles::free_balance(&T::AwtTokenId::get(), &T::SwapPoolAccount::get()) + < awt_amount.into() + { + return Ok(()); + } + T::Fungibles::transfer_token( + &T::SwapPoolAccount::get(), + T::AwtTokenId::get(), + awt_amount.into(), + &who, + )?; + T::Fungibles::transfer_token( + &who, + token_id, + unified_amount, + &T::SwapPoolAccount::get(), + )?; + Ok(()) + } + #[transactional] #[pallet::weight(195_000_000)] pub fn buy_ticket( @@ -922,7 +1432,7 @@ pub mod pallet { Error::::BattleStatusError ); ensure!( - battle.battle_type == season.first_round_battle_type, + battle.battle_type == season.first_finals_battle_type, Error::::BattleTypeError, ); ensure!(battle.season == season_id, Error::::BattleNotInSeason); @@ -967,7 +1477,8 @@ pub mod pallet { ensure!(votes.len() > 0, Error::::VoteSelectZero); let season = Self::get_season_info(season_id).ok_or(Error::::SeasonNotFound)?; ensure!( - select_battle_type < >::into(season.first_round_battle_type) + select_battle_type + < >::into(season.first_finals_battle_type) && BattleType::try_from(select_battle_type).is_ok(), Error::::BattleTypeError ); @@ -1074,38 +1585,26 @@ pub mod pallet { #[transactional] #[pallet::weight(195_000_000)] - pub fn settle(origin: OriginFor, battle_id: BattleId) -> DispatchResultWithPostInfo { + pub fn finals_settle( + origin: OriginFor, + battle_id: BattleId, + ) -> DispatchResultWithPostInfo { let _ = T::OrganizerOrigin::ensure_origin(origin)?; let battle: Battle = Self::get_battle_info(battle_id).ok_or(Error::::BattleNotFound)?; let battle_season = battle.season; let battle_type = battle.battle_type.clone(); + ensure!( + battle_type == BattleType::QuarterFinals + || battle_type == BattleType::SemiFinals + || battle_type == BattleType::Finals, + Error::::BattleTypeError + ); ensure!( battle.status == BattleStatus::Completed, Error::::BattleStatusError ); - let mut score_diff: Score = 0; - let mut winner: NpcId = 0; - let mut loser: NpcId = 0; - match battle.home_score > battle.visiting_score { - true => { - score_diff = battle.home_score.unwrap() - battle.visiting_score.unwrap(); - winner = battle.home; - loser = battle.visiting; - } - false => { - score_diff = battle.visiting_score.unwrap() - battle.home_score.unwrap(); - winner = battle.visiting; - loser = battle.home; - } - }; - Self::update_npc_point( - battle.season, - winner, - loser, - score_diff, - battle.battle_type.clone(), - )?; + let (_score_diff, winner, _loser) = battle.calc(); Self::update_participant_point(battle.season, battle_id, battle, winner)?; Battles::::mutate(battle_id, |b| { let mut battle = b.take().unwrap(); @@ -1126,9 +1625,217 @@ pub mod pallet { Ok(().into()) } + + #[transactional] + #[pallet::weight(195_000_000)] + pub fn drop_betting( + origin: OriginFor, + betting_id: BettingId, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let betting: Betting, AssetId> = + Self::get_betting_info(betting_id).ok_or(Error::::BettingNotFound)?; + ensure!(who == betting.creator, Error::::PermissonDeny); + + for o in betting.odds { + ensure!( + o.total_compensate_amount == Zero::zero(), + Error::::BettingError + ); + } + let pledge_amount = + T::Fungibles::free_balance(&betting.token_id, &betting.pledge_account); + let _ = T::Fungibles::transfer_token( + &betting.pledge_account, + betting.token_id, + pledge_amount, + &betting.creator, + )?; + Bettings::::mutate(betting_id, |b| { + let mut bb = b.take().unwrap(); + bb.total_pledge = Zero::zero(); + b.replace(bb); + }); + + for battle_id in betting.battles { + BettingByBattle::::mutate(battle_id, |v| { + let mut new_v = Vec::new(); + for i in 0..v.len() { + if v[i] != betting_id { + new_v.push(v[i]); + } + } + *v = new_v; + }); + } + Ok(().into()) + } + + #[transactional] + #[pallet::weight(195_000_000)] + pub fn update_odds( + origin: OriginFor, + betting_id: BettingId, + odds: Vec<(SelectIndex, OddsNumber)>, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let betting: Betting, AssetId> = + Self::get_betting_info(betting_id).ok_or(Error::::BettingNotFound)?; + ensure!(&who == &betting.creator, Error::::PermissonDeny); + Bettings::::mutate(betting_id, |b| -> DispatchResult { + ensure!(b.is_some(), Error::::BettingNotFound); + let mut betting = b.take().unwrap(); + for o in odds { + ensure!( + usize::from(o.0) < betting.odds.len(), + Error::::SelectIndexOverflow + ); + ensure!(o.1 > 100, Error::::BettingParamsError); + betting.odds[o.0 as usize].o = o.1; + } + Self::deposit_event(Event::BettingUpdate(betting_id, betting.clone())); + b.replace(betting); + Ok(()) + })?; + + Ok(().into()) + } + + #[transactional] + #[pallet::weight(195_000_000)] + pub fn league_settle( + origin: OriginFor, + battle_id: BattleId, + ) -> DispatchResultWithPostInfo { + let _ = T::OrganizerOrigin::ensure_origin(origin)?; + let battle: Battle = + Self::get_battle_info(battle_id).ok_or(Error::::BattleNotFound)?; + let battle_type = battle.battle_type.clone(); + ensure!( + battle.status == BattleStatus::Completed, + Error::::BattleStatusError + ); + ensure!( + battle_type == BattleType::League, + Error::::BattleTypeError + ); + let (score_diff, winner, loser) = battle.calc(); + Self::update_npc_league_point( + battle.season, + winner, + loser, + score_diff, + battle.battle_type.clone(), + )?; + Self::update_participant_point(battle.season, battle_id, battle, winner)?; + Battles::::mutate(battle_id, |b| { + let mut battle = b.take().unwrap(); + battle.status = BattleStatus::Finalized; + b.replace(battle); + }); + Ok(().into()) + } } impl Pallet { + pub fn generate_default_odd_item( + betting_type: &BettingType, + battle_size: usize, + ) -> Option>>> { + let v = [ + (3 as Score, 0 as Score), + (3 as Score, 1 as Score), + (3 as Score, 2 as Score), + (0 as Score, 3 as Score), + (1 as Score, 3 as Score), + (2 as Score, 3 as Score), + ]; + let home = 1 as HomeOrVisiting; + let visiting = 2 as HomeOrVisiting; + let w1 = [vec![home], vec![visiting]]; + let w2 = [ + vec![home, home], + vec![home, visiting], + vec![visiting, home], + vec![visiting, visiting], + ]; + let w3 = [ + vec![home, home, home], + vec![home, home, visiting], + vec![home, visiting, home], + vec![home, visiting, visiting], + vec![visiting, home, home], + vec![visiting, home, visiting], + vec![visiting, visiting, home], + vec![visiting, visiting, visiting], + ]; + + let mut r = Vec::new(); + match betting_type { + BettingType::Score => { + if battle_size != 1 { + return None; + } + for s in v { + let item = OddsItem { + win_lose: vec![], + score: vec![s], + o: 600u16, + total_compensate_amount: Zero::zero(), + buy_in: Zero::zero(), + accounts: 0, + }; + r.push(item); + } + } + BettingType::WinLose => match battle_size { + 1 => { + for o in w1 { + let item = OddsItem { + win_lose: o, + score: vec![], + o: 200u16, + total_compensate_amount: Zero::zero(), + buy_in: Zero::zero(), + accounts: 0, + }; + r.push(item); + } + } + 2 => { + for o in w2 { + let item = OddsItem { + win_lose: o, + score: vec![], + o: 400u16, + total_compensate_amount: Zero::zero(), + buy_in: Zero::zero(), + accounts: 0, + }; + r.push(item); + } + } + 3 => { + for o in w3 { + let item = OddsItem { + win_lose: o, + score: vec![], + o: 800u16, + total_compensate_amount: Zero::zero(), + buy_in: Zero::zero(), + accounts: 0, + }; + r.push(item); + } + } + _ => { + return None; + } + }, + } + Some(r) + } + pub fn append_vote_check_duplicate( old_select: &Vec, new_select: &Vec, @@ -1157,6 +1864,64 @@ pub mod pallet { None } + pub fn calc_betting_hit_index( + betting: &Betting, AssetId>, + ) -> Result { + let mut battles = Vec::new(); + for battle_id in &betting.battles { + let battle: Battle = + Self::get_battle_info(battle_id).ok_or(Error::::BattleNotFound)?; + ensure!( + battle.status == BattleStatus::Finalized, + Error::::BattleStatusError + ); + battles.push(battle); + } + match &betting.betting_type { + BettingType::Score => { + for i in 0..betting.odds.len() { + let odd = betting.odds[i].clone(); + let mut hited = true; + for j in 0..odd.score.len() { + let s = odd.score[j]; + if s.0 != battles[j].home_score.clone().unwrap() + || s.1 != battles[j].visiting_score.clone().unwrap() + { + hited = false; + break; + } + } + if hited { + return Ok(i as SelectIndex); + } + } + } + BettingType::WinLose => { + for i in 0..betting.odds.len() { + let odd = betting.odds[i].clone(); + let mut hited = true; + for j in 0..odd.win_lose.len() { + let s = odd.win_lose[j]; + let winner_home_visiting = + if battles[j].home_score > battles[j].visiting_score { + 1 as HomeOrVisiting + } else { + 2 as HomeOrVisiting + }; + if s != winner_home_visiting { + hited = false; + break; + } + } + if hited { + return Ok(i as SelectIndex); + } + } + } + } + Err(Error::::BettingError.into()) + } + pub fn addr_to_invite_code(addr: T::AccountId) -> Vec { let mut r = addr.encode(); r.reverse(); @@ -1210,28 +1975,27 @@ pub mod pallet { Ok(()) } - pub fn update_npc_point( + pub fn update_npc_league_point( season_id: SeasonId, winner: NpcId, loser: NpcId, score_diff: Score, battle_type: BattleType, ) -> DispatchResult { + if battle_type != BattleType::League { + return Ok(()); + } NpcPoints::::mutate(&season_id, &winner, |e| { e.0 = e.0 + 1; e.1 = e.1 + 1; e.2 = e.2 + 3; e.3 = e.3 + (score_diff as i32); - if battle_type == BattleType::Regular { - Self::deposit_event(Event::NpcPoints(season_id, winner, e.0, e.1, e.2, e.3)); - } + Self::deposit_event(Event::NpcPoints(season_id, winner, e.0, e.1, e.2, e.3)); }); NpcPoints::::mutate(&season_id, &loser, |e| { e.0 = e.0 + 1; e.3 = e.3 - (score_diff as i32); - if battle_type == BattleType::Regular { - Self::deposit_event(Event::NpcPoints(season_id, loser, e.0, e.1, e.2, e.3)); - } + Self::deposit_event(Event::NpcPoints(season_id, loser, e.0, e.1, e.2, e.3)); }); Ok(()) } @@ -1256,10 +2020,17 @@ pub mod pallet { Decode::decode(&mut h.as_ref()).expect("32 bytes; qed") } - pub fn date_to_timestamp(v: Vec) -> Result { + pub fn get_betting_treasury(bid: BettingId) -> T::AccountId { + let h = (b"-*-#fusotao-abyssworld-betting#-*-", bid) + .using_encoded(sp_io::hashing::blake2_256); + Decode::decode(&mut h.as_ref()).expect("32 bytes; qed") + } + + pub fn date_to_timestamp(v: Vec) -> Result { let fmt = "%Y-%m-%d %H:%M:%S"; - let dt = AsciiStr::from_ascii(&v).map_err(|_e| 0u8)?; - let s = NaiveDateTime::parse_from_str(dt.as_str(), fmt).map_err(|_e| 1u8)?; + let dt = AsciiStr::from_ascii(&v).map_err(|_e| Error::::TimeFormatError)?; + let s = NaiveDateTime::parse_from_str(dt.as_str(), fmt) + .map_err(|_e| Error::::TimeFormatError)?; let timestamp: u64 = s.timestamp() as u64; Ok(timestamp) } diff --git a/pallets/abyss-tournament/src/mock.rs b/pallets/abyss-tournament/src/mock.rs index 7d53605..d0ef3ba 100644 --- a/pallets/abyss-tournament/src/mock.rs +++ b/pallets/abyss-tournament/src/mock.rs @@ -253,6 +253,7 @@ parameter_types! { pub const MaxTicketAmount: u32 = 100; pub const MaxPariticipantPerBattle:u32 = 10000000; pub const BvbTreasury: AccountId32 = AccountId32::new([6u8; 32]); + pub const DefaultMinBetingAmount: Balance = 20_000_000_000_000_000_000; } impl pallet_abyss_tournament::Config for Test { @@ -260,13 +261,17 @@ impl pallet_abyss_tournament::Config for Test { type AwtTokenId = AwtTokenId; type BalanceConversion = Assets; type BridgeOrigin = bridge::EnsureBridge; + type BvbOrganizer = TreasuryAccount; type BvbTreasury = BvbTreasury; + type DefaultMinBetingAmount = DefaultMinBetingAmount; type DonationForAgent = DonationForAgent; type DonorAccount = DonorAccount; type MaxParticipantPerBattle = MaxPariticipantPerBattle; type MaxTicketAmount = MaxTicketAmount; + type Oracle = (); type OrganizerOrigin = frame_system::EnsureSignedBy; type RuntimeEvent = RuntimeEvent; + type SwapPoolAccount = TreasuryAccount; type TimeProvider = Timestamp; } diff --git a/pallets/abyss-tournament/src/tests.rs b/pallets/abyss-tournament/src/tests.rs index bf162e7..2d5700b 100644 --- a/pallets/abyss-tournament/src/tests.rs +++ b/pallets/abyss-tournament/src/tests.rs @@ -67,17 +67,26 @@ pub fn test_claim() { pub fn test_settle() { let alice: AccountId = AccountKeyring::Alice.into(); - assert_ok!(Tournament::settle(RuntimeOrigin::signed(TREASURY), 1,),); + assert_ok!(Tournament::finals_settle( + RuntimeOrigin::signed(TREASURY), + 1, + ),); assert_noop!( - Tournament::settle(RuntimeOrigin::signed(TREASURY), 1,), + Tournament::finals_settle(RuntimeOrigin::signed(TREASURY), 1,), Error::::BattleStatusError ); - assert_ok!(Tournament::settle(RuntimeOrigin::signed(TREASURY), 2,),); - assert_ok!(Tournament::settle(RuntimeOrigin::signed(TREASURY), 3,),); - assert_eq!(Tournament::get_npc_point(1, 1), (2, 2, 6, 6)); + assert_ok!(Tournament::finals_settle( + RuntimeOrigin::signed(TREASURY), + 2, + ),); + assert_ok!(Tournament::finals_settle( + RuntimeOrigin::signed(TREASURY), + 3, + ),); + /* assert_eq!(Tournament::get_npc_point(1, 1), (2, 2, 6, 6)); assert_eq!(Tournament::get_npc_point(1, 2), (1, 0, 0, -3)); assert_eq!(Tournament::get_npc_point(1, 3), (2, 1, 3, 0)); - assert_eq!(Tournament::get_npc_point(1, 4), (1, 0, 0, -3)); + assert_eq!(Tournament::get_npc_point(1, 4), (1, 0, 0, -3));*/ assert_eq!( Tournament::get_participant_point(&1, (&alice, 0)), (3, 2, 6) @@ -133,8 +142,8 @@ pub fn test_close_season() { (2, 50, 1041250000000000000000) ], ticket_price: 100000000000000000000, - first_round_battle_type: BattleType::SemiFinals, - current_round_battle_type: BattleType::SemiFinals, + first_finals_battle_type: BattleType::SemiFinals, + current_finals_battle_type: BattleType::SemiFinals, champion: Some(1), total_tickets: 98u32, }), @@ -939,7 +948,6 @@ pub fn init() { RuntimeOrigin::signed(TREASURY), b"sdsd".to_vec(), "2023-07-25 00:00:00".into(), - 3, BattleType::SemiFinals, 100000000000000000000 )); @@ -965,8 +973,8 @@ pub fn init() { total_battles: 3, bonus_strategy: vec![], ticket_price: 100000000000000000000, - first_round_battle_type: BattleType::SemiFinals, - current_round_battle_type: BattleType::Finals, + first_finals_battle_type: BattleType::SemiFinals, + current_finals_battle_type: BattleType::Finals, champion: None, total_tickets: 0u32, }), @@ -1010,7 +1018,7 @@ pub fn init() { Tournament::create_battle( RuntimeOrigin::signed(TREASURY), 1, - BattleType::Regular, + BattleType::League, 1, 1, "2023-07-30 00:00:00".into(), @@ -1022,7 +1030,7 @@ pub fn init() { Tournament::create_battle( RuntimeOrigin::signed(TREASURY), 4, - BattleType::Regular, + BattleType::League, 1, 2, "2023-07-30 00:00:00".into(), @@ -1035,7 +1043,7 @@ pub fn init() { Tournament::create_battle( RuntimeOrigin::signed(TREASURY), 1, - BattleType::Regular, + BattleType::League, 5, 2, "2023-07-30 00:00:00".into(), @@ -1069,3 +1077,16 @@ pub fn test_decode_invite() { let addr = Tournament::invite_code_to_addr(t); assert_eq!(a, addr.unwrap().into()); } + +#[test] +pub fn vec_remove() { + let mut v = vec![1, 2, 3, 1, 2]; + for i in 0..v.len() { + if v[i] == 3 { + v.remove(i); + break; + } + } + + println!("{:?}", v); +} diff --git a/pallets/bot/src/tests.rs b/pallets/bot/src/tests.rs index a5da944..5d18bf2 100644 --- a/pallets/bot/src/tests.rs +++ b/pallets/bot/src/tests.rs @@ -34,7 +34,7 @@ fn test_register_bot() { usdt, )); assert_ok!(pallet_fuso_token::Pallet::::mark_stable( - RawOrigin::Root.into(), + RuntimeOrigin::signed(TREASURY), usdt_id )); assert_noop!( @@ -119,7 +119,7 @@ fn test_deposit() { usdt, )); assert_ok!(pallet_fuso_token::Pallet::::mark_stable( - RawOrigin::Root.into(), + RuntimeOrigin::signed(TREASURY), usdt_id )); @@ -251,7 +251,7 @@ fn test_withdraw() { usdt, )); assert_ok!(pallet_fuso_token::Pallet::::mark_stable( - RawOrigin::Root.into(), + RuntimeOrigin::signed(TREASURY), usdt_id )); diff --git a/pallets/chainbridge-handler/src/tests.rs b/pallets/chainbridge-handler/src/tests.rs index 713dd4d..4f01bf2 100644 --- a/pallets/chainbridge-handler/src/tests.rs +++ b/pallets/chainbridge-handler/src/tests.rs @@ -660,7 +660,7 @@ fn transfer_out_charge_stable_non_native() { resource )); assert_ok!(Assets::issue(RuntimeOrigin::signed(TREASURY), denom)); - assert_ok!(Assets::mark_stable(RawOrigin::Root.into(), 1)); + assert_ok!(Assets::mark_stable(RuntimeOrigin::signed(TREASURY), 1)); let amount: Balance = 5 * DOLLARS; assert_ok!(Assets::do_mint(1, &ferdie, amount, None)); assert_ok!(Bridge::whitelist_chain( diff --git a/pallets/market/src/tests.rs b/pallets/market/src/tests.rs index a2d179f..eb1846a 100644 --- a/pallets/market/src/tests.rs +++ b/pallets/market/src/tests.rs @@ -74,7 +74,7 @@ pub fn register_market_should_work() { ), Error::::UnsupportedQuoteCurrency ); - assert_ok!(Token::mark_stable(RawOrigin::Root.into(), 2)); + assert_ok!(Token::mark_stable(RuntimeOrigin::signed(TREASURY), 2)); assert_noop!( Market::apply_for_token_listing( RuntimeOrigin::signed(ferdie.clone()), diff --git a/pallets/token/src/lib.rs b/pallets/token/src/lib.rs index ba01d83..3889049 100644 --- a/pallets/token/src/lib.rs +++ b/pallets/token/src/lib.rs @@ -192,7 +192,7 @@ pub mod pallet { #[pallet::weight(0)] pub fn mark_stable(origin: OriginFor, id: T::TokenId) -> DispatchResultWithPostInfo { - let _ = ensure_root(origin)?; + let _ = T::AdminOrigin::ensure_origin(origin)?; Tokens::::try_mutate_exists(id, |info| -> DispatchResult { ensure!(info.is_some(), Error::::TokenNotFound); let mut token_info = info.take().unwrap(); diff --git a/pallets/token/src/tests.rs b/pallets/token/src/tests.rs index 9678845..2531a5b 100644 --- a/pallets/token/src/tests.rs +++ b/pallets/token/src/tests.rs @@ -27,7 +27,7 @@ fn issuing_token_and_transfer_should_work() { ); new_test_ext().execute_with(|| { assert_ok!(Token::issue(RuntimeOrigin::signed(TREASURY), usdt,)); - assert_ok!(Token::mark_stable(RawOrigin::Root.into(), 1)); + assert_ok!(Token::mark_stable(RuntimeOrigin::signed(TREASURY), 1)); let id = 1u32; assert_eq!( Token::get_token_info(&id), diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 7bf408f..d5cbf8c 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -124,7 +124,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 202, + spec_version: 203, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 3, @@ -700,6 +700,10 @@ pub const BVB_ORGANIZER: AccountId = AccountId::new(hex_literal::hex!( "4a390a4c6da1c32cff91183a861cab95f313dafa017c5fe0a7726976097e0c4e" )); +pub const SWAP_POOL_ACCOUNT: AccountId = AccountId::new(hex_literal::hex!( + "1636e5823e75c71f9f05ec07f36ba0c7bce26e700239d032891c6e7b99a89042" +)); + pub const BVB_TREASURY: AccountId = AccountId::new(hex_literal::hex!( "760409da47a69ccab6ee4dd81ccf829fa05c14e25df43e2af556dc683f93c6dc" )); @@ -850,6 +854,9 @@ parameter_types! { pub const MaxTicketAmount: u32 = 99; pub const MaxParticipantPerBattle: u32 = 10000000; pub const BvbTreasury: AccountId = BVB_TREASURY; + pub const DefaultMinBetingAmount: Balance = 100 * TAO; + pub const BvbOrganizerAccount: AccountId = BVB_ORGANIZER; + pub const SwapPoolAccount: AccountId = SWAP_POOL_ACCOUNT; } impl pallet_abyss_tournament::Config for Runtime { @@ -857,13 +864,17 @@ impl pallet_abyss_tournament::Config for Runtime { type AwtTokenId = AwtTokenId; type BalanceConversion = Token; type BridgeOrigin = pallet_chainbridge::EnsureBridge; + type BvbOrganizer = BvbOrganizerAccount; type BvbTreasury = BvbTreasury; + type DefaultMinBetingAmount = DefaultMinBetingAmount; type DonationForAgent = DonationForAgent; type DonorAccount = DonorAccount; type MaxParticipantPerBattle = MaxParticipantPerBattle; type MaxTicketAmount = MaxTicketAmount; + type Oracle = Indicator; type OrganizerOrigin = EnsureSignedBy; type RuntimeEvent = RuntimeEvent; + type SwapPoolAccount = SwapPoolAccount; type TimeProvider = Timestamp; }