diff --git a/Cargo.lock b/Cargo.lock index 60ec1a1bb3..5e928cf09b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10324,6 +10324,7 @@ dependencies = [ "clap 4.5.17", "coerce", "futures", + "hex", "move-core-types", "moveos-types", "prometheus", diff --git a/apps/grow_bitcoin/sources/grow_information.move b/apps/grow_bitcoin/sources/grow_information.move index 07eab3c9c7..c5f8d25a73 100644 --- a/apps/grow_bitcoin/sources/grow_information.move +++ b/apps/grow_bitcoin/sources/grow_information.move @@ -117,7 +117,7 @@ module grow_bitcoin::grow_information_v3 { assert!(object::borrow(grow_project_list_obj).is_open, ErrorVoteNotOpen); let grow_project = borrow_mut_grow_project(grow_project_list_obj, id); coin_store::deposit(&mut grow_project.vote_store, coin); - let vote_detail = table::borrow_mut_with_default(&mut grow_project.vote_detail, sender(), 0); + let vote_detail = table::borrow_mut_with_default(&mut grow_project.vote_detail, address_of(account), 0); *vote_detail = *vote_detail + coin_value; grow_project.vote_value = coin_store::balance(&grow_project.vote_store); @@ -134,8 +134,8 @@ module grow_bitcoin::grow_information_v3 { value: coin_value, timestamp: now_milliseconds() }); - let point_box = mint_point_box(grow_project.id, coin_value, sender()); - object::transfer(point_box, sender()); + let point_box = mint_point_box(grow_project.id, coin_value, address_of(account)); + object::transfer(point_box, address_of(account)); } public fun get_vote(grow_project_list_obj: &Object, user: address, id: String): u256 { diff --git a/apps/invitation_record/Move.toml b/apps/invitation_record/Move.toml index 74022aaf1c..d456001521 100644 --- a/apps/invitation_record/Move.toml +++ b/apps/invitation_record/Move.toml @@ -18,3 +18,9 @@ twitter_binding = "_" std = "0x1" moveos_std = "0x2" rooch_framework = "0x3" + +[dev-addresses] +invitation_record = "0x41" +app_admin = "0x42" +gas_faucet = "0x43" +twitter_binding = "0x44" diff --git a/apps/invitation_record/sources/invitation.move b/apps/invitation_record/sources/invitation.move index 2d5db3fe2d..4060a77bad 100644 --- a/apps/invitation_record/sources/invitation.move +++ b/apps/invitation_record/sources/invitation.move @@ -3,6 +3,11 @@ module invitation_record::invitation { use std::option; use std::string::String; use std::vector; + use moveos_std::signer; + use moveos_std::consensus_codec; + use twitter_binding::tweet_v2; + use rooch_framework::bitcoin_address::BitcoinAddress; + use rooch_framework::ecdsa_k1; use moveos_std::hash; use rooch_framework::transaction; use rooch_framework::transaction::TransactionSequenceInfo; @@ -14,7 +19,7 @@ module invitation_record::invitation { use moveos_std::table_vec; use moveos_std::table_vec::TableVec; use rooch_framework::bitcoin_address; - use twitter_binding::twitter_account::{verify_and_binding_twitter_account, check_binding_tweet}; + use twitter_binding::twitter_account::{verify_and_binding_twitter_account, check_binding_tweet, check_user_claimed}; use moveos_std::tx_context::sender; use rooch_framework::account_coin_store; use rooch_framework::gas_coin::RGas; @@ -26,6 +31,9 @@ module invitation_record::invitation { use app_admin::admin::AdminCap; use moveos_std::object::{Object, to_shared, ObjectID}; use moveos_std::table::Table; + #[test_only] + use std::string; + #[test_only] use bitcoin_move::utxo; #[test_only] @@ -41,12 +49,21 @@ module invitation_record::invitation { const ErrorFaucetNotOpen: u64 = 1; const ErrorFaucetNotEnoughRGas: u64 = 2; const ErrorNoRemainingLuckeyTicket: u64 = 3; + const ErrorNoInvitationSignature: u64 = 4; + const ErrorNoInvitationBitcoinSignature: u64 = 5; + const ErrorNotClaimerAddress: u64 = 6; + const ErrorCannotInviteOneself: u64 = 7; + const ErrorInvalidSignature: u64 = 8; const ONE_RGAS: u256 = 1_00000000; + const INIT_GAS_AMOUNT: u256 = 1000000_00000000; const ErrorInvalidArg: u64 = 0; + const MessagePrefix : vector = b"Bitcoin Signed Message:\n"; + + struct UserInvitationRecords has key, store { - invitation_records: Table, + invitation_records: TableVec, lottery_records: TableVec, total_invitations: u64, remaining_luckey_ticket: u64, @@ -54,6 +71,12 @@ module invitation_record::invitation { lottery_reward_amount: u256, } + struct InvitationRecordInfo has store { + timestamp: u64, + address: address, + reward_amount: u256, + } + struct LotteryInfo has store { timestamp: u64, reward_amount: u256, @@ -66,9 +89,18 @@ module invitation_record::invitation { unit_invitation_amount: u256, } - 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 invitation_obj = object::new_named_object(InvitationConf{ - rgas_store: coin_store::create_coin_store(), + rgas_store, invitation_records: table::new(), is_open: true, unit_invitation_amount: ONE_RGAS * 5 @@ -96,12 +128,30 @@ module invitation_record::invitation { } /// Anyone can call this function to help the claimer claim the faucet - public entry fun claim_from_faucet(faucet_obj: &mut Object, invitation_obj: &mut Object, claimer: address, utxo_ids: vector, inviter: address){ + public entry fun claim_from_faucet( + faucet_obj: &mut Object, + invitation_obj: &mut Object, + claimer_bitcoin_address: String, + utxo_ids: vector, + inviter: address, + public_key: vector, + signature: vector, + message: vector, + ){ + let bitcoin_address = bitcoin_address::from_string(&claimer_bitcoin_address); + let full_message = encode_full_message(MessagePrefix, message); + verify_btc_signature(bitcoin_address, public_key, signature, full_message); + let claimer = bitcoin_address::to_rooch_address(&bitcoin_address); + assert!(inviter != claimer, ErrorCannotInviteOneself); let invitation_conf = object::borrow_mut(invitation_obj); assert!(invitation_conf.is_open, ErrorFaucetNotOpen); + if (inviter == @rooch_framework){ + claim(faucet_obj, claimer, utxo_ids); + return + }; if (!table::contains(&invitation_conf.invitation_records, inviter)) { table::add(&mut invitation_conf.invitation_records, inviter, UserInvitationRecords{ - invitation_records: table::new(), + invitation_records: table_vec::new(), lottery_records: table_vec::new(), total_invitations: 0u64, remaining_luckey_ticket: 0u64, @@ -110,8 +160,11 @@ module invitation_record::invitation { }) }; let user_invitation_records = table::borrow_mut(&mut invitation_conf.invitation_records, inviter); - let invitation_amount = table::borrow_mut_with_default(&mut user_invitation_records.invitation_records, claimer, 0u256); - *invitation_amount = *invitation_amount + invitation_conf.unit_invitation_amount; + table_vec::push_back(&mut user_invitation_records.invitation_records, InvitationRecordInfo{ + reward_amount: invitation_conf.unit_invitation_amount, + address: claimer, + timestamp: now_seconds() + }); user_invitation_records.total_invitations = user_invitation_records.total_invitations + 1u64; user_invitation_records.invitation_reward_amount = user_invitation_records.invitation_reward_amount + invitation_conf.unit_invitation_amount; user_invitation_records.remaining_luckey_ticket = user_invitation_records.remaining_luckey_ticket + 1u64; @@ -121,14 +174,31 @@ module invitation_record::invitation { claim(faucet_obj, claimer, utxo_ids); } - public entry fun claim_from_twitter(tweet_id: String, invitation_obj: &mut Object, inviter: address){ + public entry fun claim_from_twitter( + tweet_id: String, + invitation_obj: &mut Object, + inviter: address, + public_key: vector, + signature: vector, + message: vector, + ){ let bitcoin_address = check_binding_tweet(tweet_id); let claimer = bitcoin_address::to_rooch_address(&bitcoin_address); + assert!(inviter != claimer, ErrorCannotInviteOneself); + let full_message = encode_full_message(MessagePrefix, message); + verify_btc_signature(bitcoin_address, public_key, signature, full_message); let invitation_conf = object::borrow_mut(invitation_obj); assert!(invitation_conf.is_open, ErrorFaucetNotOpen); + let tweet_obj = tweet_v2::borrow_tweet_object(tweet_id); + let tweet = object::borrow(tweet_obj); + let author_id = *tweet_v2::tweet_author_id(tweet); + if (inviter == @rooch_framework || check_user_claimed(author_id)){ + verify_and_binding_twitter_account(tweet_id); + return + }; if (!table::contains(&invitation_conf.invitation_records, inviter)) { table::add(&mut invitation_conf.invitation_records, inviter, UserInvitationRecords{ - invitation_records: table::new(), + invitation_records: table_vec::new(), lottery_records: table_vec::new(), total_invitations: 0u64, remaining_luckey_ticket: 0u64, @@ -137,8 +207,11 @@ module invitation_record::invitation { }) }; let user_invitation_records = table::borrow_mut(&mut invitation_conf.invitation_records, inviter); - let invitation_amount = table::borrow_mut_with_default(&mut user_invitation_records.invitation_records, claimer, 0u256); - *invitation_amount = *invitation_amount + invitation_conf.unit_invitation_amount; + table_vec::push_back(&mut user_invitation_records.invitation_records, InvitationRecordInfo{ + reward_amount: invitation_conf.unit_invitation_amount, + address: claimer, + timestamp: now_seconds() + }); user_invitation_records.total_invitations = user_invitation_records.total_invitations + 1u64; user_invitation_records.invitation_reward_amount = user_invitation_records.invitation_reward_amount + invitation_conf.unit_invitation_amount; user_invitation_records.remaining_luckey_ticket = user_invitation_records.remaining_luckey_ticket + 1u64; @@ -169,6 +242,12 @@ module invitation_record::invitation { } } + public fun invitation_user_record(invitation_obj: &Object, account: address): &UserInvitationRecords{ + let invitation_conf = object::borrow(invitation_obj); + table::borrow(&invitation_conf.invitation_records, account) + } + + public entry fun close_invitation( invitation_obj: &mut Object, _admin: &mut Object, @@ -203,6 +282,54 @@ module invitation_record::invitation { coin_store::deposit(rgas_store, rgas_coin); } + fun encode_full_message(message_prefix: vector, message_info: vector): vector { + encode_full_message_consensus(message_prefix, message_info) + } + + fun starts_with(haystack: &vector, needle: &vector): bool { + let haystack_len = vector::length(haystack); + let needle_len = vector::length(needle); + + if (needle_len > haystack_len) { + return false + }; + + let i = 0; + while (i < needle_len) { + if (vector::borrow(haystack, i) != vector::borrow(needle, i)) { + return false + }; + i = i + 1; + }; + + true + } + + fun encode_full_message_consensus(message_prefix: vector, message_info: vector): vector { + + let encoder = consensus_codec::encoder(); + consensus_codec::emit_var_slice(&mut encoder, message_prefix); + consensus_codec::emit_var_slice(&mut encoder, message_info); + consensus_codec::unpack_encoder(encoder) + } + + public fun verify_btc_signature(bitcoin_address: BitcoinAddress, public_key: vector, signature: vector, message: vector) { + let message_hash = hash::sha2_256(message); + assert!( + ecdsa_k1::verify( + &signature, + &public_key, + &message_hash, + ecdsa_k1::sha256() + ), + ErrorNoInvitationSignature + ); + assert!( + bitcoin_address::verify_bitcoin_address_with_public_key(&bitcoin_address, &public_key), + ErrorNoInvitationBitcoinSignature + ); + } + fun seed(index: u64): vector { // get sequence number let sequence_number = tx_context::sequence_number(); @@ -255,11 +382,11 @@ module invitation_record::invitation { } - #[test(sender=@0x42)] + #[test(sender=@0xf0919849a42aa204673b15e586614963649a634851589dfbfde326816bed4161)] fun test_claim_with_invitation(sender: &signer){ bitcoin_move::genesis::init_for_test(); - create_account_for_testing(@0x42); - create_account_for_testing(@0x43); + create_account_for_testing(@0xf0919849a42aa204673b15e586614963649a634851589dfbfde326816bed4161); + create_account_for_testing(@0x7efa53965d5cdd8c3a6f69e4001a6920a53d427a8b4b99de1d1ceb8bd2e0dc5d); let invitation_obj = object::new_named_object(InvitationConf{ invitation_records: table::new(), is_open: true, @@ -276,13 +403,17 @@ module invitation_record::invitation { let sat_value = 100000000; let test_utxo = utxo::new_for_testing(tx_id, 0u32, sat_value); let test_utxo_id = object::id(&test_utxo); - utxo::transfer_for_testing(test_utxo, @0x43); - claim_from_faucet(faucet_obj, invitation_obj, @0x43, vector[test_utxo_id], @0x42); + let signature = x"5a1a4923742b43c73db01430fb0bea005eb54e9d764dbada3f00155981827ab076355636bbd89920ae12b50d91acfd8d5b31e078785afd3fd23928def8b53e41"; + let message = b"hello, rooch"; + let pk = x"02645681b3197f99f8763bccb34fab611778bf61806c2bd2fd8f335e87ed8c23fd"; + let claimer_address= string::utf8(b"bc1pewcwnlshuxedpfywzk9vztnvpj54zmd0a29ydseygtuu7kfjcm9qjngxn0"); + utxo::transfer_for_testing(test_utxo, bitcoin_address::to_rooch_address(&bitcoin_address::from_string(&claimer_address))); + claim_from_faucet(faucet_obj, invitation_obj, claimer_address, vector[test_utxo_id], @0x7efa53965d5cdd8c3a6f69e4001a6920a53d427a8b4b99de1d1ceb8bd2e0dc5d, pk, signature, message); let invitation_obj = object::borrow_mut_object_shared(object::named_object_id()); - let invitation = object::borrow(invitation_obj); - let records = table::borrow(&invitation.invitation_records, @0x42); - let invitation_user_record = table::borrow(&records.invitation_records, @0x43); - assert!(invitation_user_record == &500000000, 1); + let invitation = object::borrow(invitation_obj); + let records = table::borrow(&invitation.invitation_records, @0x7efa53965d5cdd8c3a6f69e4001a6920a53d427a8b4b99de1d1ceb8bd2e0dc5d); + let invitation_user_record = table_vec::borrow(&records.invitation_records, 0); + assert!(invitation_user_record.reward_amount == 500000000, 1); assert!(records.invitation_reward_amount == 500000000, 2); assert!(records.total_invitations == 1, 3); } diff --git a/apps/twitter_binding/sources/twitter_account.move b/apps/twitter_binding/sources/twitter_account.move index c54386038c..e61069d564 100644 --- a/apps/twitter_binding/sources/twitter_account.move +++ b/apps/twitter_binding/sources/twitter_account.move @@ -179,6 +179,11 @@ module twitter_binding::twitter_account { }; } + public fun check_user_claimed(author_id: String): bool{ + let faucet = borrow_twitter_rgas_faucet(); + return table::contains(&faucet.claim_records, author_id) + } + public entry fun unbinding_twitter_account(owner: &signer){ let user_rooch_address = signer::address_of(owner); unbinding_twitter_account_internal(user_rooch_address); diff --git a/crates/rooch-faucet/Cargo.toml b/crates/rooch-faucet/Cargo.toml index 9bb1957228..70b809c74e 100644 --- a/crates/rooch-faucet/Cargo.toml +++ b/crates/rooch-faucet/Cargo.toml @@ -43,4 +43,5 @@ moveos-types = { workspace = true } rooch-types = { workspace = true } rooch-rpc-client = { workspace = true } rooch-rpc-api = { workspace = true } +hex = "0.4.3" diff --git a/crates/rooch-faucet/src/app.rs b/crates/rooch-faucet/src/app.rs index ca18670ad2..69f3c46951 100644 --- a/crates/rooch-faucet/src/app.rs +++ b/crates/rooch-faucet/src/app.rs @@ -1,8 +1,11 @@ // Copyright (c) RoochNetwork // SPDX-License-Identifier: Apache-2.0 -use crate::{faucet_proxy::FaucetProxy, DiscordConfig, FaucetError, FaucetRequest}; +use crate::{ + faucet_proxy::FaucetProxy, DiscordConfig, FaucetError, FaucetRequest, FaucetRequestWithInviter, +}; use move_core_types::u256::U256; +use rooch_rpc_api::jsonrpc_types::UnitedAddressView; use std::sync::{atomic::AtomicBool, Arc}; use tokio::sync::{mpsc::Receiver, RwLock}; @@ -37,6 +40,24 @@ impl App { Ok(amount) } + pub async fn request_with_inviter( + &self, + request: FaucetRequestWithInviter, + ) -> Result { + let amount = self + .faucet_proxy + .claim_with_inviter( + request.claimer, + request.inviter, + request.claimer_sign, + request.public_key, + request.message, + ) + .await + .map_err(FaucetError::custom)?; + Ok(amount) + } + pub async fn check_gas_balance(&self) -> Result { let balance = self .faucet_proxy @@ -66,4 +87,25 @@ impl App { .map_err(FaucetError::custom)?; Ok(address.to_rooch_address().to_string()) } + pub async fn binding_twitter_account_with_inviter( + &self, + tweet_id: String, + inviter: UnitedAddressView, + claimer_sign: String, + public_key: String, + message: String, + ) -> Result { + let address = self + .faucet_proxy + .binding_twitter_account_with_inviter( + tweet_id, + inviter, + claimer_sign, + public_key, + message, + ) + .await + .map_err(FaucetError::custom)?; + Ok(address.to_rooch_address().to_string()) + } } diff --git a/crates/rooch-faucet/src/faucet.rs b/crates/rooch-faucet/src/faucet.rs index 16c5b9e1aa..1ca46c5254 100644 --- a/crates/rooch-faucet/src/faucet.rs +++ b/crates/rooch-faucet/src/faucet.rs @@ -1,7 +1,9 @@ // Copyright (c) RoochNetwork // SPDX-License-Identifier: Apache-2.0 -use crate::{faucet_module, tweet_fetcher_module, tweet_v2_module, twitter_account_module}; +use crate::{ + faucet_module, invitation_module, tweet_fetcher_module, tweet_v2_module, twitter_account_module, +}; use crate::{metrics::FaucetMetrics, FaucetError}; use anyhow::{bail, Result}; use async_trait::async_trait; @@ -31,9 +33,15 @@ pub struct FaucetConfig { #[clap(long)] pub faucet_module_address: ParsedAddress, + #[clap(long)] + pub invitation_module_address: ParsedAddress, + #[clap(long)] pub faucet_object_id: ObjectID, + #[clap(long)] + pub invitation_object_id: ObjectID, + /// The address to send the faucet claim transaction /// Default is the active address in the wallet #[clap(long, default_value = "default")] @@ -43,7 +51,9 @@ pub struct FaucetConfig { pub struct Faucet { faucet_sender: RoochAddress, faucet_module_address: AccountAddress, + invitation_module_address: AccountAddress, faucet_object_id: ObjectID, + invitation_object_id: ObjectID, context: WalletContext, faucet_error_sender: Sender, // metrics: FaucetMetrics, @@ -57,6 +67,18 @@ impl Message for ClaimMessage { type Result = Result; } +pub struct ClaimWithInviterMessage { + pub claimer: UnitedAddressView, + pub inviter: UnitedAddressView, + pub claimer_sign: String, + pub public_key: String, + pub message: String, +} + +impl Message for ClaimWithInviterMessage { + type Result = Result; +} + pub struct BalanceMessage; impl Message for BalanceMessage { @@ -73,6 +95,24 @@ impl Handler for Faucet { } } +#[async_trait] +impl Handler for Faucet { + async fn handle( + &mut self, + msg: ClaimWithInviterMessage, + _ctx: &mut ActorContext, + ) -> Result { + self.claim_with_inviter( + msg.claimer, + msg.inviter, + msg.claimer_sign, + msg.public_key, + msg.message, + ) + .await + } +} + #[async_trait] impl Handler for Faucet { async fn handle(&mut self, _msg: BalanceMessage, _ctx: &mut ActorContext) -> Result { @@ -118,6 +158,36 @@ impl Handler for Faucet { } } +pub struct BindingTwitterAccountMessageWithInviter { + pub tweet_id: String, + pub inviter: UnitedAddressView, + pub claimer_sign: String, + pub public_key: String, + pub message: String, +} + +impl Message for BindingTwitterAccountMessageWithInviter { + type Result = Result; +} + +#[async_trait] +impl Handler for Faucet { + async fn handle( + &mut self, + msg: BindingTwitterAccountMessageWithInviter, + _ctx: &mut ActorContext, + ) -> Result { + self.binding_twitter_account_with_inviter( + msg.tweet_id, + msg.inviter, + msg.claimer_sign, + msg.public_key, + msg.message, + ) + .await + } +} + impl Faucet { pub fn new( prometheus_registry: &Registry, @@ -127,11 +197,15 @@ impl Faucet { ) -> Result { let _metrics = FaucetMetrics::new(prometheus_registry); let faucet_module_address = wallet_context.resolve_address(config.faucet_module_address)?; + let invitation_module_address = + wallet_context.resolve_address(config.invitation_module_address)?; let faucet_sender = wallet_context.resolve_address(config.faucet_sender)?; Ok(Self { faucet_sender: faucet_sender.into(), faucet_module_address, + invitation_module_address, faucet_object_id: config.faucet_object_id, + invitation_object_id: config.invitation_object_id, context: wallet_context, faucet_error_sender, }) @@ -182,6 +256,63 @@ impl Faucet { } } + async fn claim_with_inviter( + &mut self, + claimer: UnitedAddressView, + inviter: UnitedAddressView, + claimer_sign: String, + public_key: String, + message: String, + ) -> Result { + tracing::debug!("claim address: {}, inviter address: {}", claimer, inviter); + let claimer_addr: AccountAddress = claimer.clone().into(); + let inviter_addr: AccountAddress = inviter.clone().into(); + let client = self.context.get_client().await?; + let utxo_ids = Self::get_utxos(&client, claimer.clone()).await?; + let claim_amount = Self::check_claim( + &client, + self.faucet_module_address, + self.faucet_object_id.clone(), + claimer_addr, + utxo_ids.clone(), + ) + .await?; + + let function_call = invitation_module::claim_from_faucet_function_call( + self.invitation_module_address, + self.faucet_object_id.clone(), + self.invitation_object_id.clone(), + claimer.to_string(), + utxo_ids, + inviter_addr, + public_key, + claimer_sign, + message, + ); + let action = MoveAction::Function(function_call); + let tx_data = self + .context + .build_tx_data(self.faucet_sender, action, None) + .await?; + let response = self + .context + .sign_and_execute(self.faucet_sender, tx_data) + .await?; + match response.execution_info.status { + KeptVMStatusView::Executed => { + tracing::info!("Claim success for {}", claimer); + Ok(claim_amount) + } + status => { + let err = FaucetError::Transfer(format!("{:?}", status)); + if let Err(e) = self.faucet_error_sender.try_send(err) { + tracing::warn!("Failed to send error to faucet_error_sender: {:?}", e); + } + bail!("Claim failed, Unexpected VM status: {:?}", status) + } + } + } + async fn balance(&self) -> Result { let client = self.context.get_client().await?; let function_call = @@ -298,6 +429,47 @@ impl Faucet { } } + async fn binding_twitter_account_with_inviter( + &self, + tweet_id: String, + inviter: UnitedAddressView, + claimer_sign: String, + public_key: String, + message: String, + ) -> Result { + let inviter_addr: AccountAddress = inviter.clone().into(); + self.check_tweet(tweet_id.as_str())?; + let client = self.context.get_client().await?; + let bitcoin_address = self.check_binding_tweet(tweet_id.clone()).await?; + + let function_call = invitation_module::claim_from_twitter_function_call( + self.invitation_module_address, + tweet_id, + inviter_addr, + public_key, + claimer_sign, + message, + ); + + let tx_data = self + .context + .build_tx_data( + self.faucet_sender, + MoveAction::Function(function_call), + None, + ) + .await?; + let tx = self.context.sign_transaction(self.faucet_sender, tx_data)?; + let response = client.rooch.execute_tx(tx, None).await?; + match response.execution_info.status { + KeptVMStatusView::Executed => Ok(bitcoin_address), + status => bail!( + "Verify and binding twitter account failed, Unexpected VM status: {:?}", + status + ), + } + } + fn check_tweet(&self, tweet_id: &str) -> Result<()> { if tweet_id.len() != 19 { bail!("Invalid tweet id length: {}", tweet_id.len()); diff --git a/crates/rooch-faucet/src/faucet_proxy.rs b/crates/rooch-faucet/src/faucet_proxy.rs index 7815525259..759a1da450 100644 --- a/crates/rooch-faucet/src/faucet_proxy.rs +++ b/crates/rooch-faucet/src/faucet_proxy.rs @@ -1,7 +1,7 @@ // Copyright (c) RoochNetwork // SPDX-License-Identifier: Apache-2.0 -use crate::{ClaimMessage, Faucet}; +use crate::{ClaimMessage, ClaimWithInviterMessage, Faucet}; use anyhow::Result; use coerce::actor::ActorRef; use move_core_types::u256::U256; @@ -23,6 +23,25 @@ impl FaucetProxy { self.actor.send(ClaimMessage { claimer }).await? } + pub async fn claim_with_inviter( + &self, + claimer: UnitedAddressView, + inviter: UnitedAddressView, + claimer_sign: String, + public_key: String, + message: String, + ) -> Result { + self.actor + .send(ClaimWithInviterMessage { + claimer, + inviter, + claimer_sign, + public_key, + message, + }) + .await? + } + pub async fn balance(&self) -> Result { self.actor.send(crate::BalanceMessage).await? } @@ -41,4 +60,23 @@ impl FaucetProxy { .send(crate::VerifyAndBindingTwitterAccountMessage { tweet_id }) .await? } + + pub async fn binding_twitter_account_with_inviter( + &self, + tweet_id: String, + inviter: UnitedAddressView, + claimer_sign: String, + public_key: String, + message: String, + ) -> Result { + self.actor + .send(crate::BindingTwitterAccountMessageWithInviter { + tweet_id, + inviter, + claimer_sign, + public_key, + message, + }) + .await? + } } diff --git a/crates/rooch-faucet/src/invitation_module.rs b/crates/rooch-faucet/src/invitation_module.rs new file mode 100644 index 0000000000..9353ece1dd --- /dev/null +++ b/crates/rooch-faucet/src/invitation_module.rs @@ -0,0 +1,107 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use move_core_types::account_address::AccountAddress; +use move_core_types::ident_str; +use move_core_types::identifier::IdentStr; +use move_core_types::language_storage::ModuleId; +use move_core_types::value::MoveValue; +use moveos_types::move_std::string::MoveString; +use moveos_types::move_types::FunctionId; +use moveos_types::moveos_std::object::ObjectID; +use moveos_types::state::MoveState; +use moveos_types::transaction::FunctionCall; + +pub const INVITATION_MODULE_NAME: &IdentStr = ident_str!("invitation"); + +pub const INVITER_CLAIM_FAUCET: &IdentStr = ident_str!("claim_from_faucet"); + +pub const CLAIM_FROM_TWITTER_FUNCTION: &IdentStr = ident_str!("claim_from_twitter"); +pub fn claim_from_faucet_function_call( + module_address: AccountAddress, + faucet_object_id: ObjectID, + invitation_object_id: ObjectID, + claimer: String, + utxo_ids: Vec, + inviter: AccountAddress, + public_key: String, + signature: String, + message: String, +) -> FunctionCall { + FunctionCall { + function_id: FunctionId::new( + ModuleId::new(module_address, INVITATION_MODULE_NAME.to_owned()), + INVITER_CLAIM_FAUCET.to_owned(), + ), + ty_args: vec![], + args: vec![ + faucet_object_id.to_move_value().simple_serialize().unwrap(), + invitation_object_id + .to_move_value() + .simple_serialize() + .unwrap(), + MoveString::from(claimer) + .to_move_value() + .simple_serialize() + .unwrap(), + MoveValue::Vector(utxo_ids.iter().map(|id| id.to_move_value()).collect()) + .simple_serialize() + .unwrap(), + MoveValue::Address(inviter).simple_serialize().unwrap(), + hex::decode(public_key) + .unwrap() + .to_move_value() + .simple_serialize() + .unwrap(), + hex::decode(signature) + .unwrap() + .to_move_value() + .simple_serialize() + .unwrap(), + message + .into_bytes() + .to_move_value() + .simple_serialize() + .unwrap(), + ], + } +} + +pub fn claim_from_twitter_function_call( + module_address: AccountAddress, + tweet_id: String, + inviter: AccountAddress, + public_key: String, + signature: String, + message: String, +) -> FunctionCall { + FunctionCall { + function_id: FunctionId::new( + ModuleId::new(module_address, INVITATION_MODULE_NAME.to_owned()), + CLAIM_FROM_TWITTER_FUNCTION.to_owned(), + ), + ty_args: vec![], + args: vec![ + MoveString::from(tweet_id) + .to_move_value() + .simple_serialize() + .unwrap(), + MoveValue::Address(inviter).simple_serialize().unwrap(), + hex::decode(public_key) + .unwrap() + .to_move_value() + .simple_serialize() + .unwrap(), + hex::decode(signature) + .unwrap() + .to_move_value() + .simple_serialize() + .unwrap(), + message + .into_bytes() + .to_move_value() + .simple_serialize() + .unwrap(), + ], + } +} diff --git a/crates/rooch-faucet/src/lib.rs b/crates/rooch-faucet/src/lib.rs index 1339306157..d3b2887c73 100644 --- a/crates/rooch-faucet/src/lib.rs +++ b/crates/rooch-faucet/src/lib.rs @@ -33,4 +33,5 @@ mod tweet_fetcher_module; mod tweet_v2_module; mod twitter_account_module; +mod invitation_module; pub mod server; diff --git a/crates/rooch-faucet/src/requests.rs b/crates/rooch-faucet/src/requests.rs index 9f2e86eb99..9eb82e48c6 100644 --- a/crates/rooch-faucet/src/requests.rs +++ b/crates/rooch-faucet/src/requests.rs @@ -9,12 +9,30 @@ pub struct FaucetRequest { pub claimer: UnitedAddressView, } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct FaucetRequestWithInviter { + pub claimer: UnitedAddressView, + pub inviter: UnitedAddressView, + pub claimer_sign: String, + pub public_key: String, + pub message: String, +} + impl FaucetRequest { pub fn recipient(&self) -> UnitedAddressView { self.claimer.clone() } } +impl FaucetRequestWithInviter { + pub fn recipient(&self) -> UnitedAddressView { + self.claimer.clone() + } + pub fn inviter(&self) -> UnitedAddressView { + self.inviter.clone() + } +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct FetchTweetRequest { pub tweet_id: String, @@ -24,3 +42,12 @@ pub struct FetchTweetRequest { pub struct VerifyAndBindingTwitterAccountRequest { pub tweet_id: String, } + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct VerifyAndBindingTwitterAccountWithInviter { + pub tweet_id: String, + pub inviter: UnitedAddressView, + pub claimer_sign: String, + pub public_key: String, + pub message: String, +} diff --git a/crates/rooch-faucet/src/web.rs b/crates/rooch-faucet/src/web.rs index 77dd0bb772..eee80a9493 100644 --- a/crates/rooch-faucet/src/web.rs +++ b/crates/rooch-faucet/src/web.rs @@ -2,8 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{ - App, FaucetRequest, FaucetResponse, FetchTweetRequest, InfoResponse, ResultResponse, - VerifyAndBindingTwitterAccountRequest, + App, FaucetRequest, FaucetRequestWithInviter, FaucetResponse, FetchTweetRequest, InfoResponse, + ResultResponse, VerifyAndBindingTwitterAccountRequest, + VerifyAndBindingTwitterAccountWithInviter, }; use axum::{ error_handling::HandleErrorLayer, @@ -72,11 +73,16 @@ pub async fn serve(app: App, web_config: WebConfig) -> Result<(), anyhow::Error> .route(METRICS_ROUTE, get(metrics)) .route("/info", get(request_info)) .route("/faucet", post(request_faucet)) + .route("/faucet-inviter", post(request_faucet_with_inviter)) .route("/fetch-tweet", post(fetch_tweet)) .route( "/verify-and-binding-twitter-account", post(verify_and_binding_twitter_account), ) + .route( + "/binding-twitter-with-inviter", + post(binding_twitter_account_with_inviter), + ) .layer( ServiceBuilder::new() .layer(HandleErrorLayer::new(handle_error)) @@ -142,6 +148,35 @@ async fn request_faucet( } } +async fn request_faucet_with_inviter( + Extension(app): Extension, + Json(payload): Json, +) -> impl IntoResponse { + let recipient = payload.recipient().to_string(); + let inviter = payload.inviter().to_string(); + tracing::info!( + "request gas payload: {:?} inviter: {:?}", + recipient, + inviter + ); + + let result = app.request_with_inviter(payload).await; + + match result { + Ok(amount) => { + tracing::info!("request gas success add queue: {}", recipient); + (StatusCode::CREATED, Json(FaucetResponse::from(amount))) + } + Err(e) => { + tracing::info!("request gas error: {}, {:?}", recipient, e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(FaucetResponse::from(e)), + ) + } + } +} + async fn request_info(Extension(app): Extension) -> impl IntoResponse { let result = app.check_gas_balance().await; @@ -174,6 +209,32 @@ async fn verify_and_binding_twitter_account( ResultResponse::::from(app.verify_and_binding_twitter_account(tweet_id).await) } +async fn binding_twitter_account_with_inviter( + Extension(app): Extension, + Json(payload): Json, +) -> impl IntoResponse { + let tweet_id = payload.tweet_id; + let inviter = payload.inviter.to_string(); + let claimer_sign = payload.claimer_sign; + let public_key = payload.public_key; + let message = payload.message; + tracing::info!( + "verify and binding twitter account payload: {:?} inviter:{:?}", + tweet_id, + inviter + ); + ResultResponse::::from( + app.binding_twitter_account_with_inviter( + tweet_id, + payload.inviter, + claimer_sign, + public_key, + message, + ) + .await, + ) +} + async fn handle_error(error: BoxError) -> impl IntoResponse { if error.is::() { return ( diff --git a/frameworks/moveos-stdlib/sources/tx_context.move b/frameworks/moveos-stdlib/sources/tx_context.move index a1047c5d2c..77eb1304b9 100644 --- a/frameworks/moveos-stdlib/sources/tx_context.move +++ b/frameworks/moveos-stdlib/sources/tx_context.move @@ -223,6 +223,13 @@ module moveos_std::tx_context { ctx.sequence_number = sequence_number; } + #[test_only] + /// set the TxContext tx_hash for unit test + public fun set_ctx_tx_hash_for_testing(tx_hash: vector){ + let ctx = borrow_mut(); + ctx.tx_hash = tx_hash; + } + #[test_only] public fun fresh_address_for_testing(): address { fresh_address()