How to write a telegram red packect bot on NEAR

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.

1
2
3
4
5
#[near_bindgen]
#[derive(Default, BorshDeserialize, BorshSerialize)]
pub struct LinkDrop {
pub accounts: Map<PublicKey, Balance>,
}

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 :).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#[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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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.

So the code after upgrading should be like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#[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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// deploy contract function
async function deployContract() {
const near = await connect(config);
const account = await near.account("dispa1r.testnet");
const response = await account.deployContract(fs.readFileSync('./linkdrop.wasm'));
console.log(response);
}
const public_key = [];
const nearPriAccount = [];

## generate multiple keypair
async function createKeyPair(newAccountId, num) {
const keyStore1 = new keyStores.UnencryptedFileSystemKeyStore("./");
//const creatorAccount = await near.account(creatorAccountId);

for (i = 0; i < num; i++) {
const keyPair = KeyPair.fromRandom("ed25519");
await keyStore1.setKey(config.networkId, newAccountId + i, keyPair);
const KEY_PATH = './testnet/' + newAccountId + i + ".json";
const credentials = JSON.parse(fs.readFileSync(KEY_PATH));
//keyStore.setKey(NETWORK_ID, ACCOUNT_ID, KeyPair.fromString(credentials.private_key));
public_key.push(keyPair.publicKey.toString().replace('ed25519:', ''));
nearPriAccount.push(credentials.private_key.replace('ed25519:', ''));
}
//console.log(public_key);
console.log(nearPriAccount);
//await keyStore.setKey(config.networkId, "testnmsl1.testnet", keyPair)
}

async function getContract(viewMethods = [], changeMethods = [], secretKey) {
const near = await connect(config);
if (secretKey) {
await keyStore.setKey(
NETWORK_ID, "dispa1r.testnet",
nearAPI.KeyPair.fromString(secretKey)
)
}
const tmpAccount = await near.account("dispa1r.testnet");
const contract = new nearAPI.Contract(tmpAccount, "dispa1r.testnet", {
viewMethods,
changeMethods,
sender: "dispa1r.testnet"
})
return contract
}
async function getContract1(viewMethods = [], changeMethods = []) {
const near = await connect(config);
const tmpAccount = await near.account("dispa1r.testnet");
const signAccount = await near.account("dispa1r1.testnet");
const contract1 = new nearAPI.Contract(tmpAccount, "dispa1r.testnet", {
viewMethods,
changeMethods,
sender: tmpAccount
})
return contract1
}

// 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
b.Handle("/deposit",func(m *tb.Message) {
b.Send(m.Sender, "please transfer to the near account:dispa1r.testnet")
b.Send(m.Sender, "after transfer, please input your txn hash")
b.Send(m.Sender, "the deposit must be bigger than 1 near")
b.Handle(tb.OnText, func(m *tb.Message) {
// all the text messages that weren't
// captured by existing handlers
url := "https://explorer.testnet.near.org/transactions/" + m.Text
result := Get(url)
//fmt.Println(result)
reg1 := regexp.MustCompile(`"receiverId":"(?s:(.*?)).testnet",`)
result1 := reg1.FindAllStringSubmatch(result, -1)
receiverId := result1[0][1]
reg2 := regexp.MustCompile(`"deposit":"(?s:(.*?))",`)
result1 = reg2.FindAllStringSubmatch(result, -1)
tmp := result1[0][1]
numOnly := strings.TrimSuffix(tmp, "\"}}],\"status\":\"SuccessValue")
var numOnly1 string
numOnly1 = strings.TrimSuffix(numOnly, "0000000000000000000000")
num,err := strconv.Atoi(numOnly1)
num1 := float64(num)/100
if err!=nil || num <100{
b.Send(m.Sender, "invalid number")
return
}
if receiverId != "dispa1r" {
b.Send(m.Sender, "invalid transaction")
return
}
err = common.GenerateTxn(m.Text)
if err !=nil{
b.Send(m.Sender, "invalid transaction")
return
}
common.DepositMoney(m.Sender.ID, num1)
num2 := strconv.FormatFloat(num1,'f',5,32)
str3 := "success to deposit "+num2+" near"
b.Send(m.Sender, str3)
return
})

})

Then is the send_luck function and the claim function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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
})

})

repo address : Disp41r lucky robot

conclusion

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.