From 848e07cb1d63dfa9f09370e541f17bc8aabdeeb2 Mon Sep 17 00:00:00 2001 From: jolestar Date: Fri, 27 Sep 2024 10:16:49 +0800 Subject: [PATCH] [portal] Implement gas airdrop and add testsuit to gas market (#2695) * [cli] Make balance output use symbol as key * [rpc] Make the router log to debug * [portal] Implement gas airdrop * [portal] Add testsuit to gas market and gas airdrop * fixup * [gas airdrop] Record the claim records --- crates/rooch-rpc-server/src/axum_router.rs | 4 +- .../src/commands/account/commands/balance.rs | 17 ++- .../commands/move_cli/commands/unit_test.rs | 3 +- crates/testsuite/features/multisign.feature | 2 +- crates/testsuite/features/portal.feature | 86 +++++++++++ .../contract/gas_market/Move.toml | 3 + .../gas_market/sources/gas_airdrop.move | 139 ++++++++++++++++++ .../gas_market/sources/gas_market.move | 68 ++++++--- 8 files changed, 292 insertions(+), 30 deletions(-) create mode 100644 crates/testsuite/features/portal.feature create mode 100644 infra/rooch-portal-v2/contract/gas_market/sources/gas_airdrop.move diff --git a/crates/rooch-rpc-server/src/axum_router.rs b/crates/rooch-rpc-server/src/axum_router.rs index 2592a1f881..5b152de8ef 100644 --- a/crates/rooch-rpc-server/src/axum_router.rs +++ b/crates/rooch-rpc-server/src/axum_router.rs @@ -161,7 +161,7 @@ async fn process_request(req: Request<'_>, call: CallData<'_>) -> MethodResponse let id = req.id; tracing::event!( - tracing::Level::INFO, + tracing::Level::DEBUG, event = "on_call", method = name_str, params = params_str, @@ -221,7 +221,7 @@ async fn process_request(req: Request<'_>, call: CallData<'_>) -> MethodResponse }; tracing::event!( - tracing::Level::INFO, + tracing::Level::DEBUG, event = "on_result", method = name_str, result = response.as_result(), diff --git a/crates/rooch/src/commands/account/commands/balance.rs b/crates/rooch/src/commands/account/commands/balance.rs index af337879c3..c4cdd930a6 100644 --- a/crates/rooch/src/commands/account/commands/balance.rs +++ b/crates/rooch/src/commands/account/commands/balance.rs @@ -121,15 +121,22 @@ impl CommandAction> for BalanceCommand { match balance_info { BalanceInfoViewUnion::Bitcoin(bitcoin_balance) => { balances_view.insert( - bitcoin_balance.coin_info.name.to_string(), + bitcoin_balance.coin_info.symbol.to_string(), bitcoin_balance.into(), ); } BalanceInfoViewUnion::Other(other_balance) => { - balances_view.insert( - other_balance.coin_info.coin_type.to_string(), - other_balance.into(), - ); + if balances_view.contains_key(&other_balance.coin_info.symbol) { + balances_view.insert( + other_balance.coin_info.coin_type.to_string(), + other_balance.into(), + ); + } else { + balances_view.insert( + other_balance.coin_info.symbol.to_string(), + other_balance.into(), + ); + } } } } diff --git a/crates/rooch/src/commands/move_cli/commands/unit_test.rs b/crates/rooch/src/commands/move_cli/commands/unit_test.rs index b4109e9779..1afea94493 100644 --- a/crates/rooch/src/commands/move_cli/commands/unit_test.rs +++ b/crates/rooch/src/commands/move_cli/commands/unit_test.rs @@ -77,8 +77,7 @@ impl CommandAction> for TestCommand { let resolution_graph = build_config .clone() - .resolution_graph_for_package(&root_path, &mut Vec::new()) - .expect("resolve package dep failed"); + .resolution_graph_for_package(&root_path, &mut Vec::new())?; let mut additional_named_address = BTreeMap::new(); let _: Vec<_> = resolution_graph diff --git a/crates/testsuite/features/multisign.feature b/crates/testsuite/features/multisign.feature index adfe73cff7..a54ba50929 100644 --- a/crates/testsuite/features/multisign.feature +++ b/crates/testsuite/features/multisign.feature @@ -34,7 +34,7 @@ Feature: Rooch CLI multisign integration tests Then sleep: "10" # wait for the transaction to be confirmed Then cmd: "account balance -a {{$.account[-2].account0.address}} --json" - Then assert: "{{$.account[-1].Bitcoin.balance}} == 100000000" + Then assert: "{{$.account[-1].BTC.balance}} == 100000000" #transfer some gas to multisign account Then cmd: "account transfer --to {{$.account[-2].multisign_address}} --amount 10000000000 --coin-type rooch_framework::gas_coin::RGas" diff --git a/crates/testsuite/features/portal.feature b/crates/testsuite/features/portal.feature new file mode 100644 index 0000000000..8ff2b464ab --- /dev/null +++ b/crates/testsuite/features/portal.feature @@ -0,0 +1,86 @@ +Feature: Rooch Portal contract tests + + + @serial + Scenario: gas_market + Given a bitcoind server for gas_market + Given a server for gas_market + + Then cmd: "account create --json" + Then cmd: "account create --json" + Then cmd: "account list --json" + + # mint utxos + Then cmd bitcoin-cli: "generatetoaddress 101 {{$.account[2].account0.bitcoin_address}}" + Then sleep: "10" + + Then cmd: "move run --function rooch_framework::gas_coin::faucet_entry --args u256:1000000000000000 --json" + Then assert: "{{$.move[-1].execution_info.status.type}} == executed" + + # publish gas_market via default address + Then cmd: "move publish -p ../../infra/rooch-portal-v2/contract/gas_market --named-addresses gas_market=default --json" + Then assert: "{{$.move[-1].execution_info.status.type}} == executed" + + Then cmd: "event get-events-by-event-handle -t default::trusted_oracle::NewOracleEvent" + + # submit the oracle price, require at least two record + Then cmd: "move run --function default::trusted_oracle::submit_data --args object:{{$.event[-1].data[0].decoded_event_data.value.oracle_id}} --args string:BTCUSD --args u256:5805106000000 --args u8:8 --args string:1 --args object:{{$.event[-1].data[0].decoded_event_data.value.admin_id}} --json" + Then assert: "{{$.move[-1].execution_info.status.type}} == executed" + Then cmd: "move run --function default::trusted_oracle::submit_data --args object:{{$.event[-1].data[0].decoded_event_data.value.oracle_id}} --args string:BTCUSD --args u256:5805106000000 --args u8:8 --args string:1 --args object:{{$.event[-1].data[0].decoded_event_data.value.admin_id}} --json" + Then assert: "{{$.move[-1].execution_info.status.type}} == executed" + + Then cmd: "move run --function default::trusted_oracle::submit_data --args object:{{$.event[-1].data[1].decoded_event_data.value.oracle_id}} --args string:BTCUSD --args u256:5805106000000 --args u8:8 --args string:2 --args object:{{$.event[-1].data[1].decoded_event_data.value.admin_id}} --json" + Then assert: "{{$.move[-1].execution_info.status.type}} == executed" + Then cmd: "move run --function default::trusted_oracle::submit_data --args object:{{$.event[-1].data[1].decoded_event_data.value.oracle_id}} --args string:BTCUSD --args u256:5805106000000 --args u8:8 --args string:2 --args object:{{$.event[-1].data[1].decoded_event_data.value.admin_id}} --json" + Then assert: "{{$.move[-1].execution_info.status.type}} == executed" + + Then cmd: "move run --function default::trusted_oracle::submit_data --args object:{{$.event[-1].data[2].decoded_event_data.value.oracle_id}} --args string:BTCUSD --args u256:5805106000000 --args u8:8 --args string:3 --args object:{{$.event[-1].data[2].decoded_event_data.value.admin_id}} --json" + Then assert: "{{$.move[-1].execution_info.status.type}} == executed" + Then cmd: "move run --function default::trusted_oracle::submit_data --args object:{{$.event[-1].data[2].decoded_event_data.value.oracle_id}} --args string:BTCUSD --args u256:5805106000000 --args u8:8 --args string:3 --args object:{{$.event[-1].data[2].decoded_event_data.value.admin_id}} --json" + Then assert: "{{$.move[-1].execution_info.status.type}} == executed" + + # ensure the account0 RGas balance is 0 + Then cmd: "account balance -a {{$.account[2].account0.address}} --coin-type 0x3::gas_coin::RGas --json" + Then assert: "{{$.account[-1].RGAS.balance}} == 0" + + # transfer 0.01 BTC to the gas market address + Then cmd: "bitcoin transfer -s {{$.account[2].account0.bitcoin_address}} -t {{$.account[2].default.bitcoin_address}} -a 1000000" + Then cmd bitcoin-cli: "generatetoaddress 1 {{$.account[2].account0.bitcoin_address}}" + Then sleep: "10" + + Then cmd: "object -t default::gas_market::RGasMarket" + # consume the utxo event + Then cmd: "move run --function default::gas_market::consume_event --args object:{{$.object[-1].data[0].id}} --json" + Then assert: "{{$.move[-1].execution_info.status.type}} == executed" + + # check the RGas balance of account0 + Then cmd: "account balance -a {{$.account[2].account0.address}} --coin-type 0x3::gas_coin::RGas --json" + Then assert: "{{$.account[-1].RGAS.balance}} != 0" + + # ensure the account1 RGas balance is 0 + Then cmd: "account balance -a {{$.account[2].account1.address}} --coin-type 0x3::gas_coin::RGas --json" + Then assert: "{{$.account[-1].RGAS.balance}} == 0" + + # transfer 2 utxo to account1 + Then cmd: "bitcoin transfer -s {{$.account[2].account0.bitcoin_address}} -t {{$.account[2].account1.bitcoin_address}} -a 1000000" + Then cmd: "bitcoin transfer -s {{$.account[2].account0.bitcoin_address}} -t {{$.account[2].account1.bitcoin_address}} -a 1000000" + Then cmd bitcoin-cli: "generatetoaddress 1 {{$.account[2].account0.bitcoin_address}}" + Then sleep: "10" + + Then cmd: "object -t default::gas_airdrop::RGasAirdrop" + Then cmd: "object -o {{$.account[2].account1.bitcoin_address}} -t 0x4::utxo::UTXO" + + # the default address help the account1 to claim the airdrop + Then cmd: "move run --function default::gas_airdrop::claim --args object:{{$.object[-2].data[0].id}} --args address:{{$.account[2].account1.address}} --args vector:{{$.object[-1].data[0].id}},{{$.object[-1].data[1].id}} --json" + Then assert: "{{$.move[-1].execution_info.status.type}} == executed" + + # check the RGas balance of account0 + Then cmd: "account balance -a {{$.account[2].account1.address}} --coin-type 0x3::gas_coin::RGas --json" + Then assert: "{{$.account[-1].RGAS.balance}} != 0" + + # try claim again + Then cmd: "move run --function default::gas_airdrop::claim --args object:{{$.object[-2].data[0].id}} --args address:{{$.account[2].account1.address}} --args vector:{{$.object[-1].data[0].id}},{{$.object[-1].data[1].id}} --json" + Then assert: "{{$.move[-1].execution_info.status.type}} != executed" + + + diff --git a/infra/rooch-portal-v2/contract/gas_market/Move.toml b/infra/rooch-portal-v2/contract/gas_market/Move.toml index 6ddd4e9759..31223a9159 100644 --- a/infra/rooch-portal-v2/contract/gas_market/Move.toml +++ b/infra/rooch-portal-v2/contract/gas_market/Move.toml @@ -15,3 +15,6 @@ std = "0x1" moveos_std = "0x2" rooch_framework = "0x3" bitcoin_move = "0x4" + +[dev-addresses] +gas_market = "0x42" diff --git a/infra/rooch-portal-v2/contract/gas_market/sources/gas_airdrop.move b/infra/rooch-portal-v2/contract/gas_market/sources/gas_airdrop.move new file mode 100644 index 0000000000..590fa7aef8 --- /dev/null +++ b/infra/rooch-portal-v2/contract/gas_market/sources/gas_airdrop.move @@ -0,0 +1,139 @@ +module gas_market::gas_airdrop { + + use std::vector; + + use moveos_std::signer; + use moveos_std::object::{Self, Object, ObjectID, to_shared}; + use moveos_std::tx_context::{sender}; + use moveos_std::table::{Self, Table}; + + use rooch_framework::coin_store::CoinStore; + use rooch_framework::gas_coin::RGas; + use rooch_framework::coin_store; + use rooch_framework::account_coin_store; + + use bitcoin_move::utxo::{Self, UTXO}; + + use gas_market::gas_market::AdminCap; + + const INIT_GAS_AMOUNT: u256 = 5000000_00000000; + const ONE_RGAS: u256 = 1_00000000; + + //0.01 BTC + const SAT_LEVEL_ONE: u64 = 1000000; + //0.1 BTC + const SAT_LEVEL_TWO: u64 = 10000000; + + const ErrorAirdropNotOpen: u64 = 0; + const ErrorInvalidUTXO: u64 = 1; + const ErrorAirdropNotEnoughRGas: u64 = 2; + const ErrorAlreadyClaimed: u64 = 3; + const ErrorUTXOValueIsZero: u64 = 4; + + + struct RGasAirdrop has key{ + rgas_store: Object>, + claim_records: Table, + is_open: bool + } + + + fun init(sender: &signer) { + let sender_addr = signer::address_of(sender); + let rgas_store = coin_store::create_coin_store(); + let rgas_balance = account_coin_store::balance(sender_addr); + let airdrop_gas_amount = if(rgas_balance > INIT_GAS_AMOUNT) { + INIT_GAS_AMOUNT + } else { + rgas_balance/3 + }; + Self::deposit_to_rgas_store(sender, &mut rgas_store, airdrop_gas_amount); + let rgas_airdrop_obj = object::new_named_object(RGasAirdrop{ + rgas_store, + claim_records: table::new(), + is_open: true + }); + to_shared(rgas_airdrop_obj) + } + + /// Anyone can call this function to help the claimer claim the airdrop + public entry fun claim(airdrop_obj: &mut Object, claimer: address, utxo_ids: vector){ + let airdrop = object::borrow_mut(airdrop_obj); + assert!(!table::contains(&airdrop.claim_records, claimer), ErrorAlreadyClaimed); + + let total_sat_amount = Self::total_sat_amount(claimer, utxo_ids); + assert!(total_sat_amount > 0, ErrorUTXOValueIsZero); + let claim_rgas_amount = Self::sat_amount_to_rgas(total_sat_amount); + let remaining_rgas_amount = coin_store::balance(&airdrop.rgas_store); + assert!(claim_rgas_amount <= remaining_rgas_amount, ErrorAirdropNotEnoughRGas); + let rgas_coin = coin_store::withdraw(&mut airdrop.rgas_store, claim_rgas_amount); + account_coin_store::deposit(claimer, rgas_coin); + table::add(&mut airdrop.claim_records, claimer, claim_rgas_amount); + } + + public entry fun deposit_rgas_coin( + account: &signer, + rgas_airdrop_obj: &mut Object, + amount: u256 + ){ + let rgas_airdrop = object::borrow_mut(rgas_airdrop_obj); + deposit_to_rgas_store(account, &mut rgas_airdrop.rgas_store, amount); + } + + public entry fun withdraw_rgas_coin( + _admin: &mut Object, + rgas_airdrop_obj: &mut Object, + amount: u256 + ){ + let rgas_airdrop = object::borrow_mut(rgas_airdrop_obj); + let rgas_coin = coin_store::withdraw(&mut rgas_airdrop.rgas_store, amount); + account_coin_store::deposit(sender(), rgas_coin); + } + + public entry fun close_airdrop( + _admin: &mut Object, + rgas_airdrop_obj: &mut Object + ){ + let rgas_airdrop = object::borrow_mut(rgas_airdrop_obj); + rgas_airdrop.is_open = false; + } + + + fun deposit_to_rgas_store( + account: &signer, + rgas_store: &mut Object>, + amount: u256 + ){ + let rgas_coin = account_coin_store::withdraw(account, amount); + coin_store::deposit(rgas_store, rgas_coin); + } + + /// A view function to get the amount of RGas that can be claimed + public fun get_claimable_rgas(claimer: address, utxo_ids: vector): u256 { + let total_sat_amount = Self::total_sat_amount(claimer, utxo_ids); + Self::sat_amount_to_rgas(total_sat_amount) + } + + fun sat_amount_to_rgas(sat_amount: u64): u256{ + if (sat_amount == 0){ + 0 + }else if(sat_amount <= SAT_LEVEL_ONE){ + ONE_RGAS + }else if(sat_amount <= SAT_LEVEL_TWO){ + 2 * ONE_RGAS + }else{ + 3 * ONE_RGAS + } + } + + fun total_sat_amount(claim_address: address, utxo_ids: vector): u64{ + let total_sat_amount = 0; + vector::for_each(utxo_ids, |utxo_id| { + let utxo_obj = object::borrow_object(utxo_id); + assert!(object::owner(utxo_obj) == claim_address, ErrorInvalidUTXO); + total_sat_amount = total_sat_amount + utxo::value(object::borrow(utxo_obj)); + }); + total_sat_amount + } + +} diff --git a/infra/rooch-portal-v2/contract/gas_market/sources/gas_market.move b/infra/rooch-portal-v2/contract/gas_market/sources/gas_market.move index 3e7342a40e..23445b40f5 100644 --- a/infra/rooch-portal-v2/contract/gas_market/sources/gas_market.move +++ b/infra/rooch-portal-v2/contract/gas_market/sources/gas_market.move @@ -1,21 +1,26 @@ module gas_market::gas_market { use std::option; use std::string::utf8; + use moveos_std::decimal_value::value; - use gas_market::trusted_oracle::trusted_price; use moveos_std::address::to_string; - use bitcoin_move::utxo::{ReceiveUTXOEvent, unpack_receive_utxo_event}; use moveos_std::event_queue::{Subscriber, consume}; use moveos_std::event_queue; use moveos_std::table; use moveos_std::table::Table; - use rooch_framework::account_coin_store; use moveos_std::tx_context::sender; use moveos_std::object::{Object, to_shared, transfer}; - use rooch_framework::coin_store::CoinStore; use moveos_std::object; + use moveos_std::signer; + + use rooch_framework::coin_store::{Self, CoinStore}; use rooch_framework::gas_coin::RGas; - use rooch_framework::coin_store; + use rooch_framework::account_coin_store; + + use bitcoin_move::utxo::{ReceiveUTXOEvent, unpack_receive_utxo_event}; + + use gas_market::trusted_oracle::trusted_price; + #[test_only] use moveos_std::decimal_value; @@ -26,6 +31,7 @@ module gas_market::gas_market { unit_price: u256, receive_btc_address: address, market_info: MarketInfo, + utxo_subscriber: Object>, is_open: bool } @@ -41,42 +47,51 @@ module gas_market::gas_market { const DEFAULT_UNIT_PRICE: u256 = 1000000; const BTC_USD: vector = b"BTCUSD"; + const INIT_GAS_AMOUNT: u256 = 50000000_00000000; + const ErrorMarketNotOpen: u64 = 0; const ErrorReceiverAddress: u64 = 1; const ErrorTokenPrice: u64 = 2; const ErrorNoUncheckTxid: u64 = 3; - fun init() { + fun init(sender: &signer) { + let sender_addr = signer::address_of(sender); + let rgas_store = coin_store::create_coin_store(); + let rgas_balance = account_coin_store::balance(sender_addr); + let market_gas_amount = if (rgas_balance > INIT_GAS_AMOUNT) { + INIT_GAS_AMOUNT + } else { + rgas_balance / 3 + }; + deposit_to_rgas_store(sender, &mut rgas_store, market_gas_amount); + let utxo_subscriber = event_queue::subscribe(to_string(&sender())); let rgas_market_obj = object::new_named_object( RGasMarket { - rgas_store: coin_store::create_coin_store(), + rgas_store, unit_price: DEFAULT_UNIT_PRICE, - receive_btc_address: sender(), + receive_btc_address: sender_addr, market_info: MarketInfo { total_deposit: 0, total_withdraw: 0, buyer: table::new(), uncheck_info: table::new() }, + utxo_subscriber, is_open: true } ); let admin_cap = object::new_named_object(AdminCap {}); - to_shared(event_queue::subscribe(to_string(&sender()))); - transfer(admin_cap, sender()); + transfer(admin_cap, sender_addr); to_shared(rgas_market_obj) } - public entry fun add_rgas_coin( + public entry fun deposit_rgas_coin( account: &signer, rgas_market_obj: &mut Object, amount: u256 ) { let rgas_market = object::borrow_mut(rgas_market_obj); - assert!(rgas_market.is_open, ErrorMarketNotOpen); - let rgas_coin = account_coin_store::withdraw(account, amount); - coin_store::deposit(&mut rgas_market.rgas_store, rgas_coin); - + deposit_to_rgas_store(account, &mut rgas_market.rgas_store, amount); rgas_market.market_info.total_deposit = rgas_market.market_info.total_deposit + amount } @@ -86,8 +101,6 @@ module gas_market::gas_market { amount: u256 ) { let rgas_market = object::borrow_mut(rgas_market_obj); - assert!(rgas_market.is_open, ErrorMarketNotOpen); - let rgas_coin = coin_store::withdraw(&mut rgas_market.rgas_store, amount); account_coin_store::deposit(sender(), rgas_coin); rgas_market.market_info.total_withdraw = rgas_market.market_info.total_withdraw @@ -96,11 +109,10 @@ module gas_market::gas_market { public entry fun consume_event( rgas_market_obj: &mut Object, - subscriber_obj: &mut Object> ) { let rgas_market = object::borrow_mut(rgas_market_obj); assert!(rgas_market.is_open, ErrorMarketNotOpen); - let consume_event = option::extract(&mut consume(subscriber_obj)); + let consume_event = option::extract(&mut consume(&mut rgas_market.utxo_subscriber)); let (txid, sender, receiver, value) = unpack_receive_utxo_event(consume_event); assert!(receiver == rgas_market.receive_btc_address, ErrorReceiverAddress); let withdraw_amount = btc_to_rgas(value); @@ -145,7 +157,6 @@ module gas_market::gas_market { amount: u256 ) { let rgas_market = object::borrow_mut(rgas_market_obj); - assert!(rgas_market.is_open, ErrorMarketNotOpen); let rgas_coin = coin_store::withdraw(&mut rgas_market.rgas_store, amount); account_coin_store::deposit(sender_addr, rgas_coin); assert!( @@ -180,6 +191,14 @@ module gas_market::gas_market { table::remove(&mut rgas_market.market_info.uncheck_info, txid); } + public entry fun close_market( + _admin: &mut Object, + rgas_market_obj: &mut Object + ) { + let rgas_market = object::borrow_mut(rgas_market_obj); + rgas_market.is_open = false; + } + public fun btc_to_rgas(sats_amount: u64): u256 { let price_info = trusted_price(utf8(BTC_USD)); let btc_price = value(&price_info); @@ -193,6 +212,15 @@ module gas_market::gas_market { rgas_amount * DEFAULT_UNIT_PRICE / token_price } + fun deposit_to_rgas_store( + account: &signer, + rgas_store: &mut Object>, + amount: u256 + ){ + let rgas_coin = account_coin_store::withdraw(account, amount); + coin_store::deposit(rgas_store, rgas_coin); + } + #[test] fun test_btc_to_rgas() { let price_info = decimal_value::new(5005206000000, 8);