In this tutorial, i will show you how to write a telegram red packect of near. There will be two parts, the contract and the robot part :)
Ready to work
Before develop on near, you should have a testnet account to deploy and call the contract. You can register it at NEAR testnet.
And then, you should have near-cli and rust tool chain on your computer.
1 2 3 4 5 6 7
# install rust tool chain curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh rustup target add wasm32-unknown-unknown # install near-cli npm install near-cli -g # login the near account near login
Contract
You may need a little experience in rust development, but if you are someone who is not familiar with the rust language, but has a certain foundation of object-oriented programming, you don’t need to worry, because the contract code becomes very readable with the help of comments。
First we can find there has been a linkdrop contract deployed on both near testnet and mainnet. And we can find the repo of linkdrop here.
First, we need to to analyse the code of contract. First, we need to understand how it stores the data. We should read the struct part first.
The contract uses a map to store the public key and balance, it means how many near can someone who owns the private key corresponding to the public key get.Let us analyse the send function first :).
#[payable] pub fn send(&mut self, public_key: Base58PublicKey) -> Promise { assert!( env::attached_deposit() > ACCESS_KEY_ALLOWANCE, "Attached deposit must be greater than ACCESS_KEY_ALLOWANCE" ); let pk = public_key.into(); let value = self.accounts.get(&pk).unwrap_or(0); // insert the public key and balance into contract self.accounts.insert( &pk, &(value + env::attached_deposit() - ACCESS_KEY_ALLOWANCE), ); // the call back function Promise::new(env::current_account_id()).add_access_key( pk, ACCESS_KEY_ALLOWANCE, env::current_account_id(), b"claim,create_account_and_claim".to_vec(), ) }
We can see that the parameter of the call is a public key, so before we call this function, we need to use near sdk to create a valid near key pair, and we seed the public key to the contract, give the private key to the people who we want to give the linkdrop. Let us see what the add_access_key function does.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
/// Add an access key that is restricted to only calling a smart contract on some account using /// only a restricted set of methods. Here `method_names` is a comma separated list of methods, /// e.g. `b"method_a,method_b"`. pub fn add_access_key( self, public_key: PublicKey, allowance: Balance, receiver_id: AccountId, method_names: Vec<u8>, ) -> Self { self.add_action(PromiseAction::AddAccessKey { public_key, allowance, receiver_id, method_names, }) }
This function will add a access key to the contract account. Public_key is the key we just created, people who use the corresponding private key to sign the transaction could call the claim function and create_account_and_claim function. So we can know the linkdrop contract use a keypair to confirm the safety of the linkdrop. People who owns the private key could get the balance of the drop.
a little upgrade
We can see that now the contract only can give one people linkdrop, maybe we could make it more cool. We can transform the contract to allow multiple users to receive the same red envelope, it’s the same as WeChat’s lucky red envelope. So we should add some different functions.
First, we should change the parameter: from single Base58PublicKey to a vector Vec<Base58PublicKey>, and set how many people can receive. Then we just need to change adding a record to the map from before to add multiple records. So, the sendLuck function should become as following:
pub fn send_luck(&mut self, public_key: Vec<Base58PublicKey>, num: u128) -> Promise { assert!( env::attached_deposit() > ACCESS_KEY_ALLOWANCE * num, "Attached deposit must be greater than ACCESS_KEY_ALLOWANCE" ); let mut copy = public_key; let mut number = 0; let mut person = num; // evenly divide the balance let mut avgAmount = remain_num / person; while number != num { let pk = copy.pop().unwrap().into(); let value = self.accounts.get(&pk).unwrap_or(0); let third = v.pop(); self.accounts.insert( &pk, avgAmount), ); Promise::new(env::current_account_id()).add_access_key( pk, ACCESS_KEY_ALLOWANCE, env::current_account_id(), b"claim,create_account_and_claim".to_vec(), ); number += 1; } Promise::new(env::current_account_id()) }
But we should make the game more exciting. We should make the amount of red envelopes received by everyone random. At the same time, the amount of red envelopes received by each person should be satisfied with the mean distribution.
Therefore, we can implement a double mean method, but the random number part may be unfortunate. In order to meet the consensus, there is no real random number on the blockchain, but you can rely on the oracle of chainlink to obtain a secure random number. However, the NEAR chain currently does not support chainlink. We believe in that it should be possible to use chainlink NEAR in the near future, so we use block_timeStamp to get a pseudo-random number.
#[payable] pub fn send_luck(&mut self, public_key: Vec<Base58PublicKey>, num: u128) -> Promise { assert!( env::attached_deposit() > ACCESS_KEY_ALLOWANCE * num, "Attached deposit must be greater than ACCESS_KEY_ALLOWANCE" ); let mut copy = public_key; let mut number = 0; let mut remain_num = env::attached_deposit() - num * ACCESS_KEY_ALLOWANCE; let mut v:Vec<u128> = Vec::new(); let mut person = num; while number != num-1 { let mut avgAmount = remain_num / person; let mut doubleAvgAmount = avgAmount * 2; person -= 1; let mut min = ACCESS_MIN_MONEY; let mut max = doubleAvgAmount ; let mut rand = MyRandomGenerator::default(); let mut timestamp = env::block_timestamp() as u128; timestamp = timestamp % 100; let mut currentAmount =(rand.gen::<u128>() / timestamp) % max + min ; v.push(currentAmount); remain_num = remain_num - currentAmount; number += 1; } v.push(remain_num); number = 0; while number != num { let pk = copy.pop().unwrap().into(); let value = self.accounts.get(&pk).unwrap_or(0); let third = v.pop(); self.accounts.insert( &pk, &(value + third.unwrap()), ); Promise::new(env::current_account_id()).add_access_key( pk, ACCESS_KEY_ALLOWANCE, env::current_account_id(), b"claim,create_account_and_claim".to_vec(), ); number += 1; } Promise::new(env::current_account_id()) }
Aha, we just finished to upgrade the contract, we don’t need to change the claim function beacuse we don’t change the storage structure.
call the contract
Due to space reasons, how to install and use near-js-sdk will not be introduced here, I will directly show how to call our contract in the node-js environment.
// beginning send function async function callSend(public_key, deposit) { const contract = await getContract1([], ['send']) const depositNum = toNear(deposit) await contract.send({ public_key, }, 200000000000000, depositNum) .then(() => { console.log('Drop claimed') }) .catch((e) => { console.log(e) console.log('Unable to claim drop. The drop may have already been claimed.') }) }
// send Luck version async function callSendLuck(nearAmount, num) { await createKeyPair("test.testnet", num); const contract = await getContract1([], ['send_luck']) const deposit = toNear(nearAmount); //console.log(public_key); await contract.send_luck({ public_key, num, }, 200000000000000, deposit) .then(() => { console.log('Drop claimed') }) .catch((e) => { console.log(e) console.log('Unable to claim drop. The drop may have already been claimed.') }) }
async function claimDrop(account_id, privKey) { const contract = await getContract([], ['claim', 'create_account_and_claim'], privKey) // return funds to current user await contract.claim({ account_id, }, 200000000000000) .then(() => { console.log('Drop claimed') }) .catch((e) => { console.log(e) console.log('Unable to claim drop. The drop may have already been claimed.') }) }
Robot part
I do not intend to introduce the robot part in detail, because I used someone else’s golang-based telegram robot framework. It is very simple. You can find all of my code on github. If you have any questions, you can directly send me a private message via Twitter.
I think i should briefly introduce the robot-related code, i just make some comments.
The way I implemented it is very inelegant, because near does not have go-api, but in fact you can choose api-server, but I didn’t know these at the time, so I chose to go through near-js-api and go. I calling the command line command to implement the call contract. You can see the related code in utils.go and the js code in test.js.
In my robot, people only could send red packet after sign in and deposit near to the robot account, so i write a simple spider to check the transaction of deposit.
b.Handle("/lucky",func(m *tb.Message) { //chat := m.Chat b.Send(m.Sender, "input the number of the near:") var amount float64
b.Handle(tb.OnText, func(m *tb.Message) { amount,err = strconv.ParseFloat(m.Text,10) result := common.CheckIfHave(m.Sender.ID,amount) if err !=nil || !result{ b.Send(m.Sender, "invalid number, please go to deposit") } b.Send(m.Sender, "input the number of the red-packets:") b.Handle(tb.OnText, func(m *tb.Message) { var num int num,err = strconv.Atoi(m.Text) if err !=nil{ b.Send(m.Sender, "invalid number") } var privateKey []string err,privateKey = CallSendLuckCmd(amount,num) if err!=nil{ common.SendLuck(m.Sender.ID,amount) } b.Send(m.Sender, "success to call the send luck,now give u the private key") for i := range privateKey{ b.Send(m.Sender, privateKey[i]) } return }) }) })
b.Handle("/claim",func(m *tb.Message) { b.Send(m.Sender, "please input the private key to claim the drop") result := common.CheckBinded(m.Sender.ID) if !result{ b.Send(m.Sender,"please first bind the near account") return } b.Handle(tb.OnText, func(m *tb.Message) { privateKey := m.Text accountId := common.GetAccountId(m.Sender.ID) err = CallClaim(accountId,privateKey) //result := common.CheckIfHave(m.Sender.ID,amount) if err !=nil || !result{ b.Send(m.Sender, "fail to claim the drop") } b.Send(m.Sender, "success to claim the drop!") return })
If you are not familiar with the rust language, or even programming, then don’t worry, you can go to github to find other people’s code.
There are many interesting contracts in near-example, and they are also very helpful for you to learn how to develop on near.
After understanding the contract, try to modify it, then compile and deploy, gradually you will find that your understanding of it will be deeper. All in all, there is only one sentence, the industry is good at diligence, and more hands-on operations will make you become stronger.