Skip to content

Commit

Permalink
feat: add invitation for gas faucet (#3008)
Browse files Browse the repository at this point in the history
* feat: add invitation for gas faucet

* Move.toml

* add License

* cargo fmt

* check sign in contract && check if the user claimed twitter

* cargo fmt

* fix: encode full message for message_info

* fix: invitation

* fix: rust function call

* fix: invitation

* fix: claimer address

* update testcase && remove prefix

* fix: function call

* clear legacy code

* fix: find record with address

* update record struct

---------

Co-authored-by: jolestar <[email protected]>
  • Loading branch information
mx819812523 and jolestar authored Dec 6, 2024
1 parent e186728 commit c7fa35c
Show file tree
Hide file tree
Showing 14 changed files with 628 additions and 29 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions apps/grow_bitcoin/sources/grow_information.move
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<GrowProjectList>, user: address, id: String): u256 {
Expand Down
6 changes: 6 additions & 0 deletions apps/invitation_record/Move.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
173 changes: 152 additions & 21 deletions apps/invitation_record/sources/invitation.move
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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]
Expand All @@ -41,19 +49,34 @@ 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<u8> = b"Bitcoin Signed Message:\n";


struct UserInvitationRecords has key, store {
invitation_records: Table<address, u256>,
invitation_records: TableVec<InvitationRecordInfo>,
lottery_records: TableVec<LotteryInfo>,
total_invitations: u64,
remaining_luckey_ticket: u64,
invitation_reward_amount: u256,
lottery_reward_amount: u256,
}

struct InvitationRecordInfo has store {
timestamp: u64,
address: address,
reward_amount: u256,
}

struct LotteryInfo has store {
timestamp: u64,
reward_amount: u256,
Expand All @@ -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<RGas>();
let rgas_balance = account_coin_store::balance<RGas>(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>(),
rgas_store,
invitation_records: table::new(),
is_open: true,
unit_invitation_amount: ONE_RGAS * 5
Expand Down Expand Up @@ -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<RGasFaucet>, invitation_obj: &mut Object<InvitationConf>, claimer: address, utxo_ids: vector<ObjectID>, inviter: address){
public entry fun claim_from_faucet(
faucet_obj: &mut Object<RGasFaucet>,
invitation_obj: &mut Object<InvitationConf>,
claimer_bitcoin_address: String,
utxo_ids: vector<ObjectID>,
inviter: address,
public_key: vector<u8>,
signature: vector<u8>,
message: vector<u8>,
){
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,
Expand All @@ -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;
Expand All @@ -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<InvitationConf>, inviter: address){
public entry fun claim_from_twitter(
tweet_id: String,
invitation_obj: &mut Object<InvitationConf>,
inviter: address,
public_key: vector<u8>,
signature: vector<u8>,
message: vector<u8>,
){
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,
Expand All @@ -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;
Expand Down Expand Up @@ -169,6 +242,12 @@ module invitation_record::invitation {
}
}

public fun invitation_user_record(invitation_obj: &Object<InvitationConf>, 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<InvitationConf>,
_admin: &mut Object<AdminCap>,
Expand Down Expand Up @@ -203,6 +282,54 @@ module invitation_record::invitation {
coin_store::deposit(rgas_store, rgas_coin);
}

fun encode_full_message(message_prefix: vector<u8>, message_info: vector<u8>): vector<u8> {
encode_full_message_consensus(message_prefix, message_info)
}

fun starts_with(haystack: &vector<u8>, needle: &vector<u8>): 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<u8>, message_info: vector<u8>): vector<u8> {

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<u8>, signature: vector<u8>, message: vector<u8>) {
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<u8> {
// get sequence number
let sequence_number = tx_context::sequence_number();
Expand Down Expand Up @@ -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,
Expand All @@ -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<InvitationConf>(object::named_object_id<InvitationConf>());
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);
}
Expand Down
5 changes: 5 additions & 0 deletions apps/twitter_binding/sources/twitter_account.move
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions crates/rooch-faucet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Loading

0 comments on commit c7fa35c

Please sign in to comment.