The Linkdrop Smart Contract
Let's take a look at the linkdrop smart contract. The first thing we see is the Cargo.toml file; and we see the near-sdk version is "0.9.2"! So expect lots of stuffs to be so old it expires. We shall upgrade it to near-sdk-rs 4.0.0-pre.4
version.
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::U128;
use near_sdk::{
env, ext_contract, near_bindgen, AccountId, Balance,
Promise, PromiseResult, PublicKey, require, is_promise_success,
};
use near_sdk::collections::LookupMap;
use near_helper::{expect_lightweight};
// use std::collections::HashMap;
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LinkDrop {
pub accounts: LookupMap<PublicKey, Balance>,
}
impl Default for LinkDrop {
fn default() -> Self {
Self {
accounts: LookupMap::new(b"d".to_vec())
}
}
}
/// Access key allowance for linkdrop keys: 1 NEAR.
const ACCESS_KEY_ALLOWANCE: u128 = 1_000_000_000_000_000_000_000_000;
/// Gas attached to the callback from account creation: 20 TGas.
pub const ON_CREATE_ACCOUNT_CALLBACK_GAS: u64 = 20_000_000_000_000;
const NO_DEPOSIT: u128 = 0;
use const_format::formatcp;
const ERR_MSG_SEND: &str = formatcp!(
"Attached deposit must be greater than {ACCESS_KEY_ALLOWANCE}."
);
#[ext_contract(ext_self)]
pub trait ExtLinkDrop {
/// Callback after creating account and claiming linkdrop.
fn on_account_created_and_claimed(
&mut self,
amount: U128
) -> bool;
}
#[near_bindgen]
impl LinkDrop {
/// Allows given public key to claim sent balance.
/// Takes ACCESS_KEY_ALLOWANCE as fee from deposit to cover
/// account creation via an access key.
#[payable]
pub fn send(
&mut self,
public_key: PublicKey
) -> Promise {
require!(
env::attached_deposit() > ACCESS_KEY_ALLOWANCE,
ERR_MSG_SEND, // FIRST CHANGE
);
let value = self.accounts.get(&public_key).unwrap_or(0);
self.accounts.insert(
&public_key,
// SECOND CHANGE
&(value + env::attached_deposit()),
// &(value + env::attached_deposit() - ACCESS_KEY_ALLOWANCE),
);
Promise::new(env::current_account_id()).add_access_key(
public_key,
ACCESS_KEY_ALLOWANCE,
env::current_account_id(),
"claim,create_account_and_claim".to_owned(),
)
}
/// Returns the balance associated with given key.
pub fn get_key_balance(
&self,
key: PublicKey
) -> U128 {
expect_lightweight(
self.accounts.get(&key.into()),
"Key is missing"
).into()
}
}
// =============================== TESTS ======================== //
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
use near_sdk::test_utils::VMContextBuilder;
use near_sdk::{testing_env, VMContext, PublicKey};
use std::convert::TryInto;
fn linkdrop() -> AccountId {
"linkdrop.near".parse().unwrap()
}
fn bob() -> AccountId {
"bob.near".parse().unwrap()
}
fn publickey_1() -> PublicKey {
"ed25519:qSq3LoufLvTCTNGC3LJePMDGrok8dHMQ5A1YD9psbiz"
.parse()
.unwrap()
}
#[test]
fn test_get_balance_success() {
let mut contract = LinkDrop::default();
let pk: PublicKey = publickey_1();
let deposit = ACCESS_KEY_ALLOWANCE * 100;
testing_env!(VMContextBuilder::new()
.current_account_id(linkdrop())
.attached_deposit(deposit)
.build()
);
// Send
contract.send(pk.clone());
// get balance and assert eq.
let balance: u128 = contract.get_key_balance(
pk
).try_into().unwrap();
assert_eq!(
balance,
deposit
);
}
}
There's also an import called near_helper
, which is a super simple library one wrote for functions that one keeps getting on and on. You can look at it here. Otherwise, you can copy and paste the source code into internal.rs
instead. In the next page we shall temporarily divert and look a bit at how to go about with Cargo, if you don't know yet.
The first thing to note is some of the directory changes, so our import directory also changes. Another is there's no more near_sdk::collections::Map
exists: it has changed to other choices. For full support, use std::collections::HashMap
. We use LookupMap
from NEAR Collections though, because if you think about it, we are going to retrieve based on a specific key (account name or public key); and we don't need the extra functionality of iteration offered by UnorderedMap
. Because it can't derive(Default)
, we need to impl Default
. For simplicity we used a vector to define the saving key seen in LookupMap::new
; the newest practices uses enum
to define instead; but since we only have one of this we shall be lazy.
From near v4 onwards, we don't need this line anymore:
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
as it's initialized automatically. Moving on:
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::U128;
use near_sdk::{
env, ext_contract, near_bindgen, AccountId, Balance,
Promise, PromiseResult, PublicKey, require, is_promise_success,
};
use near_sdk::collections::LookupMap;
use near_helper::{expect_lightweight};
// use std::collections::HashMap;
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LinkDrop {
pub accounts: LookupMap<PublicKey, Balance>,
}
impl Default for LinkDrop {
fn default() -> Self {
Self {
accounts: LookupMap::new(b"d".to_vec())
}
}
}
/// Access key allowance for linkdrop keys: 1 NEAR.
const ACCESS_KEY_ALLOWANCE: u128 = 1_000_000_000_000_000_000_000_000;
/// Gas attached to the callback from account creation: 20 TGas.
pub const ON_CREATE_ACCOUNT_CALLBACK_GAS: u64 = 20_000_000_000_000;
const NO_DEPOSIT: u128 = 0;
use const_format::formatcp;
const ERR_MSG_SEND: &str = formatcp!(
"Attached deposit must be greater than {ACCESS_KEY_ALLOWANCE}."
);
#[ext_contract(ext_self)]
pub trait ExtLinkDrop {
/// Callback after creating account and claiming linkdrop.
fn on_account_created_and_claimed(
&mut self,
amount: U128
) -> bool;
}
#[near_bindgen]
impl LinkDrop {
/// Allows given public key to claim sent balance.
/// Takes ACCESS_KEY_ALLOWANCE as fee from deposit to cover
/// account creation via an access key.
#[payable]
pub fn send(
&mut self,
public_key: PublicKey
) -> Promise {
require!(
env::attached_deposit() > ACCESS_KEY_ALLOWANCE,
ERR_MSG_SEND, // FIRST CHANGE
);
let value = self.accounts.get(&public_key).unwrap_or(0);
self.accounts.insert(
&public_key,
// SECOND CHANGE
&(value + env::attached_deposit()),
// &(value + env::attached_deposit() - ACCESS_KEY_ALLOWANCE),
);
Promise::new(env::current_account_id()).add_access_key(
public_key,
ACCESS_KEY_ALLOWANCE,
env::current_account_id(),
"claim,create_account_and_claim".to_owned(),
)
}
/// Returns the balance associated with given key.
pub fn get_key_balance(
&self,
key: PublicKey
) -> U128 {
expect_lightweight(
self.accounts.get(&key.into()),
"Key is missing"
).into()
}
}
// =============================== TESTS ======================== //
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
use near_sdk::test_utils::VMContextBuilder;
use near_sdk::{testing_env, VMContext, PublicKey};
use std::convert::TryInto;
fn linkdrop() -> AccountId {
"linkdrop.near".parse().unwrap()
}
fn bob() -> AccountId {
"bob.near".parse().unwrap()
}
fn publickey_1() -> PublicKey {
"ed25519:qSq3LoufLvTCTNGC3LJePMDGrok8dHMQ5A1YD9psbiz"
.parse()
.unwrap()
}
#[test]
fn test_get_balance_success() {
let mut contract = LinkDrop::default();
let pk: PublicKey = publickey_1();
let deposit = ACCESS_KEY_ALLOWANCE * 100;
testing_env!(VMContextBuilder::new()
.current_account_id(linkdrop())
.attached_deposit(deposit)
.build()
);
// Send
contract.send(pk.clone());
// get balance and assert eq.
let balance: u128 = contract.get_key_balance(
pk
).try_into().unwrap();
assert_eq!(
balance,
deposit
);
}
}
The linkdrop contract only stores the list of available linkdrop public keys and their corresponding balance that's deposited to that link.
Now, comes the problem with ACCESS_KEY_ALLOWANCE
as we discussed before: it's 1 NEAR. That means, a single linkdrop need at least 1 NEAR to make the drop; which is just stupid. One just wants to deposit 0.1 NEAR, and that cannot make it.
Apparently, if you use the flaw contract and ACCESS_KEY_ALLOWANCE
, if you send 2 NEAR like we did before, your friend you get 1.9 NEAR; Of course, for mainnet, your friend would still get 2 NEAR. Unfortunately, the mainnet contract isn't available on github; only the testnet. One isn't sure where the ACCESS_KEY_ALLOWANCE
goes, though. They might have make it as small as possible so actually mainnet also deducts ACCESS_KEY_ALLOWANCE
but it's negligible.
What's still available in both testnet and mainnet, is you still need to send at least 1 NEAR. In mainnet, that will work as it don't eats your NEAR; on testnet, when you try to create the account, smart contract will panic. You send 1 NEAR, but it requires \[ \text{ACCESS_KEY_ALLOWANCE} + \text{storage} + \text{transaction} \approx 1.1 \text{NEAR} \].
The cross contract calls: these are contract calling itself.
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::U128;
use near_sdk::{
env, ext_contract, near_bindgen, AccountId, Balance,
Promise, PromiseResult, PublicKey, require, is_promise_success,
};
use near_sdk::collections::LookupMap;
use near_helper::{expect_lightweight};
// use std::collections::HashMap;
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LinkDrop {
pub accounts: LookupMap<PublicKey, Balance>,
}
impl Default for LinkDrop {
fn default() -> Self {
Self {
accounts: LookupMap::new(b"d".to_vec())
}
}
}
/// Access key allowance for linkdrop keys: 1 NEAR.
const ACCESS_KEY_ALLOWANCE: u128 = 1_000_000_000_000_000_000_000_000;
/// Gas attached to the callback from account creation: 20 TGas.
pub const ON_CREATE_ACCOUNT_CALLBACK_GAS: u64 = 20_000_000_000_000;
const NO_DEPOSIT: u128 = 0;
use const_format::formatcp;
const ERR_MSG_SEND: &str = formatcp!(
"Attached deposit must be greater than {ACCESS_KEY_ALLOWANCE}."
);
#[ext_contract(ext_self)]
pub trait ExtLinkDrop {
/// Callback after creating account and claiming linkdrop.
fn on_account_created_and_claimed(
&mut self,
amount: U128
) -> bool;
}
#[near_bindgen]
impl LinkDrop {
/// Allows given public key to claim sent balance.
/// Takes ACCESS_KEY_ALLOWANCE as fee from deposit to cover
/// account creation via an access key.
#[payable]
pub fn send(
&mut self,
public_key: PublicKey
) -> Promise {
require!(
env::attached_deposit() > ACCESS_KEY_ALLOWANCE,
ERR_MSG_SEND, // FIRST CHANGE
);
let value = self.accounts.get(&public_key).unwrap_or(0);
self.accounts.insert(
&public_key,
// SECOND CHANGE
&(value + env::attached_deposit()),
// &(value + env::attached_deposit() - ACCESS_KEY_ALLOWANCE),
);
Promise::new(env::current_account_id()).add_access_key(
public_key,
ACCESS_KEY_ALLOWANCE,
env::current_account_id(),
"claim,create_account_and_claim".to_owned(),
)
}
/// Returns the balance associated with given key.
pub fn get_key_balance(
&self,
key: PublicKey
) -> U128 {
expect_lightweight(
self.accounts.get(&key.into()),
"Key is missing"
).into()
}
}
// =============================== TESTS ======================== //
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
use near_sdk::test_utils::VMContextBuilder;
use near_sdk::{testing_env, VMContext, PublicKey};
use std::convert::TryInto;
fn linkdrop() -> AccountId {
"linkdrop.near".parse().unwrap()
}
fn bob() -> AccountId {
"bob.near".parse().unwrap()
}
fn publickey_1() -> PublicKey {
"ed25519:qSq3LoufLvTCTNGC3LJePMDGrok8dHMQ5A1YD9psbiz"
.parse()
.unwrap()
}
#[test]
fn test_get_balance_success() {
let mut contract = LinkDrop::default();
let pk: PublicKey = publickey_1();
let deposit = ACCESS_KEY_ALLOWANCE * 100;
testing_env!(VMContextBuilder::new()
.current_account_id(linkdrop())
.attached_deposit(deposit)
.build()
);
// Send
contract.send(pk.clone());
// get balance and assert eq.
let balance: u128 = contract.get_key_balance(
pk
).try_into().unwrap();
assert_eq!(
balance,
deposit
);
}
}
Let's look at linkdrop implementation. Note that we removed some of the functions that we don't want. Particularly, these are the functions for the user creating the linkdrop to claim it themselves. We dropped these aiming to reduce contract size.
The first function is the send
function. This function is for the link creator to "send funds to a temporary account for lockup until the account is claimed".
#[near_bindgen]
impl LinkDrop {
/// Allows given public key to claim sent balance.
/// Takes ACCESS_KEY_ALLOWANCE as fee from deposit to cover
/// account creation via an access key.
#[payable]
pub fn send(
&mut self,
public_key: PublicKey
) -> Promise {
require!(
env::attached_deposit() > ACCESS_KEY_ALLOWANCE,
format!(
"Attached deposit {} must be greater than {}",
env::attached_deposit(),
ACCESS_KEY_ALLOWANCE,
).to_string()
);
let value = self.accounts.get(&public_key).unwrap_or(0);
self.accounts.insert(
&public_key,
&(value + env::attached_deposit()),
// &(value + env::attached_deposit() - ACCESS_KEY_ALLOWANCE),
);
Promise::new(env::current_account_id()).add_access_key(
public_key,
ACCESS_KEY_ALLOWANCE,
env::current_account_id(),
"claim,create_account_and_claim".to_owned(),
)
}
}
Two things to look here. We want to eliminate as much format!
as possible, as it takes up more space in the compiled wasm file. So, let's use a library and make it a constant.
We add this under Cargo.toml dependencies:
[dependencies]
near_sdk = "=4.0.0-pre.4"
const_format = "0.2.22"
Then we use the formatcp!
to create a specific constant for this error message. We shall eliminate printing what the user input as the user should know what they had inputted. The other variable is another constant, so we're good to make the string a constant. However, Rust string concatenation can only happened on owned String
, not immutable &str
. Trying to go with format!
is trying to run against a river current, fighting against to compiler to no avail. Somebody made the library to deal with this, so we just use their success and build upon.
Import the library and make the constant.
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::U128;
use near_sdk::{
env, ext_contract, near_bindgen, AccountId, Balance,
Promise, PromiseResult, PublicKey, require, is_promise_success,
};
use near_sdk::collections::LookupMap;
use near_helper::{expect_lightweight};
// use std::collections::HashMap;
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LinkDrop {
pub accounts: LookupMap<PublicKey, Balance>,
}
impl Default for LinkDrop {
fn default() -> Self {
Self {
accounts: LookupMap::new(b"d".to_vec())
}
}
}
/// Access key allowance for linkdrop keys: 1 NEAR.
const ACCESS_KEY_ALLOWANCE: u128 = 1_000_000_000_000_000_000_000_000;
/// Gas attached to the callback from account creation: 20 TGas.
pub const ON_CREATE_ACCOUNT_CALLBACK_GAS: u64 = 20_000_000_000_000;
const NO_DEPOSIT: u128 = 0;
use const_format::formatcp;
const ERR_MSG_SEND: &str = formatcp!(
"Attached deposit must be greater than {ACCESS_KEY_ALLOWANCE}."
);
#[ext_contract(ext_self)]
pub trait ExtLinkDrop {
/// Callback after creating account and claiming linkdrop.
fn on_account_created_and_claimed(
&mut self,
amount: U128
) -> bool;
}
#[near_bindgen]
impl LinkDrop {
/// Allows given public key to claim sent balance.
/// Takes ACCESS_KEY_ALLOWANCE as fee from deposit to cover
/// account creation via an access key.
#[payable]
pub fn send(
&mut self,
public_key: PublicKey
) -> Promise {
require!(
env::attached_deposit() > ACCESS_KEY_ALLOWANCE,
ERR_MSG_SEND, // FIRST CHANGE
);
let value = self.accounts.get(&public_key).unwrap_or(0);
self.accounts.insert(
&public_key,
// SECOND CHANGE
&(value + env::attached_deposit()),
// &(value + env::attached_deposit() - ACCESS_KEY_ALLOWANCE),
);
Promise::new(env::current_account_id()).add_access_key(
public_key,
ACCESS_KEY_ALLOWANCE,
env::current_account_id(),
"claim,create_account_and_claim".to_owned(),
)
}
/// Returns the balance associated with given key.
pub fn get_key_balance(
&self,
key: PublicKey
) -> U128 {
expect_lightweight(
self.accounts.get(&key.into()),
"Key is missing"
).into()
}
}
// =============================== TESTS ======================== //
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
use near_sdk::test_utils::VMContextBuilder;
use near_sdk::{testing_env, VMContext, PublicKey};
use std::convert::TryInto;
fn linkdrop() -> AccountId {
"linkdrop.near".parse().unwrap()
}
fn bob() -> AccountId {
"bob.near".parse().unwrap()
}
fn publickey_1() -> PublicKey {
"ed25519:qSq3LoufLvTCTNGC3LJePMDGrok8dHMQ5A1YD9psbiz"
.parse()
.unwrap()
}
#[test]
fn test_get_balance_success() {
let mut contract = LinkDrop::default();
let pk: PublicKey = publickey_1();
let deposit = ACCESS_KEY_ALLOWANCE * 100;
testing_env!(VMContextBuilder::new()
.current_account_id(linkdrop())
.attached_deposit(deposit)
.build()
);
// Send
contract.send(pk.clone());
// get balance and assert eq.
let balance: u128 = contract.get_key_balance(
pk
).try_into().unwrap();
assert_eq!(
balance,
deposit
);
}
}
(if your VSCode shows error importing like mine does, just ignore it. As long as compilation is successful, most likely VSCode forgot to look for it; but it won't affect cargo successfully looking for the crates.)
Second, one suspects because we do the - ACCESS_KEY_ALLOWANCE
, it causes the error we discussed earlier (discovering less than what we expect to discover). We shall try to minus it out and test if it can success or not. Even if it fails, we can just uncomment out the contract; we know if we minus out it'll pass: it's being used in linkdrop contract anyways.
One wants to show a test here, but just the send
function is insufficient. In fact, let's try to create a view function called get_key_balance
to ensure it has been inserted and we can create test for it.
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::U128;
use near_sdk::{
env, ext_contract, near_bindgen, AccountId, Balance,
Promise, PromiseResult, PublicKey, require, is_promise_success,
};
use near_sdk::collections::LookupMap;
use near_helper::{expect_lightweight};
// use std::collections::HashMap;
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LinkDrop {
pub accounts: LookupMap<PublicKey, Balance>,
}
impl Default for LinkDrop {
fn default() -> Self {
Self {
accounts: LookupMap::new(b"d".to_vec())
}
}
}
/// Access key allowance for linkdrop keys: 1 NEAR.
const ACCESS_KEY_ALLOWANCE: u128 = 1_000_000_000_000_000_000_000_000;
/// Gas attached to the callback from account creation: 20 TGas.
pub const ON_CREATE_ACCOUNT_CALLBACK_GAS: u64 = 20_000_000_000_000;
const NO_DEPOSIT: u128 = 0;
use const_format::formatcp;
const ERR_MSG_SEND: &str = formatcp!(
"Attached deposit must be greater than {ACCESS_KEY_ALLOWANCE}."
);
#[ext_contract(ext_self)]
pub trait ExtLinkDrop {
/// Callback after creating account and claiming linkdrop.
fn on_account_created_and_claimed(
&mut self,
amount: U128
) -> bool;
}
#[near_bindgen]
impl LinkDrop {
/// Allows given public key to claim sent balance.
/// Takes ACCESS_KEY_ALLOWANCE as fee from deposit to cover
/// account creation via an access key.
#[payable]
pub fn send(
&mut self,
public_key: PublicKey
) -> Promise {
require!(
env::attached_deposit() > ACCESS_KEY_ALLOWANCE,
ERR_MSG_SEND, // FIRST CHANGE
);
let value = self.accounts.get(&public_key).unwrap_or(0);
self.accounts.insert(
&public_key,
// SECOND CHANGE
&(value + env::attached_deposit()),
// &(value + env::attached_deposit() - ACCESS_KEY_ALLOWANCE),
);
Promise::new(env::current_account_id()).add_access_key(
public_key,
ACCESS_KEY_ALLOWANCE,
env::current_account_id(),
"claim,create_account_and_claim".to_owned(),
)
}
/// Returns the balance associated with given key.
pub fn get_key_balance(
&self,
key: PublicKey
) -> U128 {
expect_lightweight(
self.accounts.get(&key.into()),
"Key is missing"
).into()
}
}
// =============================== TESTS ======================== //
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
use near_sdk::test_utils::VMContextBuilder;
use near_sdk::{testing_env, VMContext, PublicKey};
use std::convert::TryInto;
fn linkdrop() -> AccountId {
"linkdrop.near".parse().unwrap()
}
fn bob() -> AccountId {
"bob.near".parse().unwrap()
}
fn publickey_1() -> PublicKey {
"ed25519:qSq3LoufLvTCTNGC3LJePMDGrok8dHMQ5A1YD9psbiz"
.parse()
.unwrap()
}
#[test]
fn test_get_balance_success() {
let mut contract = LinkDrop::default();
let pk: PublicKey = publickey_1();
let deposit = ACCESS_KEY_ALLOWANCE * 100;
testing_env!(VMContextBuilder::new()
.current_account_id(linkdrop())
.attached_deposit(deposit)
.build()
);
// Send
contract.send(pk.clone());
// get balance and assert eq.
let balance: u128 = contract.get_key_balance(
pk
).try_into().unwrap();
assert_eq!(
balance,
deposit
);
}
}
If you haven't already know, a function with
&mut self
is a change function and those with&self
is treated as view functions. If you don't need the&mut self
for change function hence you use&self
, as of writing, NEAR blockchain will mistreat your change function as view function hence you cannot call it as a change function.
The final function:
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::U128;
use near_sdk::{
env, ext_contract, near_bindgen, AccountId, Balance,
Promise, PromiseResult, PublicKey, require, is_promise_success,
};
use near_sdk::collections::LookupMap;
use near_helper::{expect_lightweight};
// use std::collections::HashMap;
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LinkDrop {
pub accounts: LookupMap<PublicKey, Balance>,
}
impl Default for LinkDrop {
fn default() -> Self {
Self {
accounts: LookupMap::new(b"d".to_vec())
}
}
}
/// Access key allowance for linkdrop keys: 1 NEAR.
const ACCESS_KEY_ALLOWANCE: u128 = 1_000_000_000_000_000_000_000_000;
/// Gas attached to the callback from account creation: 20 TGas.
pub const ON_CREATE_ACCOUNT_CALLBACK_GAS: u64 = 20_000_000_000_000;
const NO_DEPOSIT: u128 = 0;
use const_format::formatcp;
const ERR_MSG_SEND: &str = formatcp!(
"Attached deposit must be greater than {ACCESS_KEY_ALLOWANCE}."
);
#[ext_contract(ext_self)]
pub trait ExtLinkDrop {
/// Callback after creating account and claiming linkdrop.
fn on_account_created_and_claimed(
&mut self,
amount: U128
) -> bool;
}
#[near_bindgen]
impl LinkDrop {
/// Allows given public key to claim sent balance.
/// Takes ACCESS_KEY_ALLOWANCE as fee from deposit to cover
/// account creation via an access key.
#[payable]
pub fn send(
&mut self,
public_key: PublicKey
) -> Promise {
require!(
env::attached_deposit() > ACCESS_KEY_ALLOWANCE,
ERR_MSG_SEND, // FIRST CHANGE
);
let value = self.accounts.get(&public_key).unwrap_or(0);
self.accounts.insert(
&public_key,
// SECOND CHANGE
&(value + env::attached_deposit()),
// &(value + env::attached_deposit() - ACCESS_KEY_ALLOWANCE),
);
Promise::new(env::current_account_id()).add_access_key(
public_key,
ACCESS_KEY_ALLOWANCE,
env::current_account_id(),
"claim,create_account_and_claim".to_owned(),
)
}
/// Returns the balance associated with given key.
pub fn get_key_balance(
&self,
key: PublicKey
) -> U128 {
expect_lightweight(
self.accounts.get(&key.into()),
"Key is missing"
).into()
}
}
// =============================== TESTS ======================== //
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
use near_sdk::test_utils::VMContextBuilder;
use near_sdk::{testing_env, VMContext, PublicKey};
use std::convert::TryInto;
fn linkdrop() -> AccountId {
"linkdrop.near".parse().unwrap()
}
fn bob() -> AccountId {
"bob.near".parse().unwrap()
}
fn publickey_1() -> PublicKey {
"ed25519:qSq3LoufLvTCTNGC3LJePMDGrok8dHMQ5A1YD9psbiz"
.parse()
.unwrap()
}
#[test]
fn test_get_balance_success() {
let mut contract = LinkDrop::default();
let pk: PublicKey = publickey_1();
let deposit = ACCESS_KEY_ALLOWANCE * 100;
testing_env!(VMContextBuilder::new()
.current_account_id(linkdrop())
.attached_deposit(deposit)
.build()
);
// Send
contract.send(pk.clone());
// get balance and assert eq.
let balance: u128 = contract.get_key_balance(
pk
).try_into().unwrap();
assert_eq!(
balance,
deposit
);
}
}
Then, we shall create the test case for this and test it runs well. As usual, we need to setup the first time we test with necessary functions and imports. Since tests aren't included during wasm compilation, we don't need to use require!
and all other tricks to reduce contract size.
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::U128;
use near_sdk::{
env, ext_contract, near_bindgen, AccountId, Balance,
Promise, PromiseResult, PublicKey, require, is_promise_success,
};
use near_sdk::collections::LookupMap;
use near_helper::{expect_lightweight};
// use std::collections::HashMap;
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LinkDrop {
pub accounts: LookupMap<PublicKey, Balance>,
}
impl Default for LinkDrop {
fn default() -> Self {
Self {
accounts: LookupMap::new(b"d".to_vec())
}
}
}
/// Access key allowance for linkdrop keys: 1 NEAR.
const ACCESS_KEY_ALLOWANCE: u128 = 1_000_000_000_000_000_000_000_000;
/// Gas attached to the callback from account creation: 20 TGas.
pub const ON_CREATE_ACCOUNT_CALLBACK_GAS: u64 = 20_000_000_000_000;
const NO_DEPOSIT: u128 = 0;
use const_format::formatcp;
const ERR_MSG_SEND: &str = formatcp!(
"Attached deposit must be greater than {ACCESS_KEY_ALLOWANCE}."
);
#[ext_contract(ext_self)]
pub trait ExtLinkDrop {
/// Callback after creating account and claiming linkdrop.
fn on_account_created_and_claimed(
&mut self,
amount: U128
) -> bool;
}
#[near_bindgen]
impl LinkDrop {
/// Allows given public key to claim sent balance.
/// Takes ACCESS_KEY_ALLOWANCE as fee from deposit to cover
/// account creation via an access key.
#[payable]
pub fn send(
&mut self,
public_key: PublicKey
) -> Promise {
require!(
env::attached_deposit() > ACCESS_KEY_ALLOWANCE,
ERR_MSG_SEND, // FIRST CHANGE
);
let value = self.accounts.get(&public_key).unwrap_or(0);
self.accounts.insert(
&public_key,
// SECOND CHANGE
&(value + env::attached_deposit()),
// &(value + env::attached_deposit() - ACCESS_KEY_ALLOWANCE),
);
Promise::new(env::current_account_id()).add_access_key(
public_key,
ACCESS_KEY_ALLOWANCE,
env::current_account_id(),
"claim,create_account_and_claim".to_owned(),
)
}
/// Returns the balance associated with given key.
pub fn get_key_balance(
&self,
key: PublicKey
) -> U128 {
expect_lightweight(
self.accounts.get(&key.into()),
"Key is missing"
).into()
}
}
// =============================== TESTS ======================== //
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
use near_sdk::test_utils::VMContextBuilder;
use near_sdk::{testing_env, VMContext, PublicKey};
use std::convert::TryInto;
fn linkdrop() -> AccountId {
"linkdrop.near".parse().unwrap()
}
fn bob() -> AccountId {
"bob.near".parse().unwrap()
}
fn publickey_1() -> PublicKey {
"ed25519:qSq3LoufLvTCTNGC3LJePMDGrok8dHMQ5A1YD9psbiz"
.parse()
.unwrap()
}
#[test]
fn test_get_balance_success() {
let mut contract = LinkDrop::default();
let pk: PublicKey = publickey_1();
let deposit = ACCESS_KEY_ALLOWANCE * 100;
testing_env!(VMContextBuilder::new()
.current_account_id(linkdrop())
.attached_deposit(deposit)
.build()
);
// Send
contract.send(pk.clone());
// get balance and assert eq.
let balance: u128 = contract.get_key_balance(
pk
).try_into().unwrap();
assert_eq!(
balance,
deposit
);
}
}
Ignoring all the warnings, we got:
Finished test [unoptimized + debuginfo] target(s) in 5.77s
Running unittests (target/debug/deps/linkdrop-0bce3ee0ea5281ec)
running 1 test
test tests::test_get_balance_success ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
As we mentioned before, if it fails and we need to minus by the ACCESS_KEY_ALLOWANCE, we can always change that later, including the tests.
Next, we shall look at claim
. This function isn't necessary actually, and we're never using it. It exists solely because unit tests requires it; if we remove the corresponding tests from the unit tests, we can remove this function.