Skip to content

Commit

Permalink
feat: add allowance to whitelist and normal sale (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
willemneal authored Jan 17, 2022
1 parent 6901de3 commit 1f57f61
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 71 deletions.
48 changes: 48 additions & 0 deletions __test__/allowance.ava.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ONE_NEAR, Workspace } from "near-willem-workspaces-ava";
import { NEAR } from "near-units";
import {
claim,
createLinkdrop,
deploy,
getTokens,
mint,
mint_raw,
sleep,
totalCost,
} from "./util";

const sale_price = NEAR.parse("0.8 N");
const allowance = 2;

const runner = Workspace.init(
{ initialBalance: NEAR.parse("20 N").toString() },
async ({ root }) => {
const alice = await root.createAccount("alice");
const tenk = await deploy(root, "tenk", {
base_cost: sale_price,
min_cost: sale_price,
allowance,
});
return { tenk, alice };
}
);

runner.test("allowance should allow only 2 tokens", async (t, { root, tenk, alice }) => {
const cost = await totalCost(tenk, 1, alice.accountId);
await mint(tenk, alice, cost);
await mint(tenk, alice, cost);
let last_try = await mint_raw(tenk, alice, cost);
t.assert(last_try.failed);
const tokens = await getTokens(tenk, alice);
t.assert(tokens.length == 2);
});

runner.test("owner has no allowance", async (t, { root, tenk, alice }) => {
const cost = await totalCost(tenk, 1, alice.accountId);
await mint(tenk, root, cost);
await mint(tenk, root, cost);
let last_try = await mint_raw(tenk, root, cost);
t.assert(last_try.succeeded);
const tokens = await getTokens(tenk, root);
t.assert(tokens.length == 3);
});
68 changes: 40 additions & 28 deletions __test__/premint.ava.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "./util";

const sale_price = NEAR.parse("0.8 N");
const allowance = 2;

const runner = Workspace.init(
{ initialBalance: NEAR.parse("20 N").toString() },
Expand All @@ -21,50 +22,61 @@ const runner = Workspace.init(
is_premint_over: false,
base_cost: sale_price,
min_cost: sale_price,
allowance,
});
return { tenk, alice };
}
);
// This is currently hard to test without fast forwarding so I think the best to test it fails then add delay.

async function premint_period<T>(
{ tenk, root, duration, base_cost },
fn: () => Promise<T>
): Promise<T> {
await root.call(tenk, "start_premint", { duration });
const sleepTimer = sleep(1000 * duration);
const res = await fn();
await sleepTimer;
let min_cost = base_cost;
await root.call(tenk, "end_premint", { base_cost, min_cost });
return res;
}

runner.test("premint", async (t, { root, tenk, alice }) => {
const cost = await totalCost(tenk, 1, alice.accountId);
const token = await mint(tenk, root);
const duration = 20;
const linkkeys = await createLinkdrop(t, tenk, root);
await claim(t, tenk, alice, linkkeys);
await root.call(tenk, "start_premint", { duration });
const sleepTimer = sleep(1000 * duration);
await t.throwsAsync(
root.call(tenk, "end_premint", {
base_cost: ONE_NEAR,
min_cost: ONE_NEAR,
})
);
const base_cost = ONE_NEAR;

let initial_try = await mint_raw(tenk, alice, cost);
t.assert(initial_try.failed);
// owner can still mint
const second_token = await mint_raw(tenk, root);
await premint_period({ tenk, root, duration, base_cost }, async () => {
await t.throwsAsync(
root.call(tenk, "end_premint", {
base_cost,
min_cost: base_cost,
})
);

await root.call(tenk, "add_whitelist_account", {
account_id: alice,
allowance: 2,
});
await mint(tenk, alice, cost);
await mint(tenk, alice, cost);
let last_try = await mint_raw(tenk, alice, cost);
t.assert(last_try.failed);
const tokens = await getTokens(tenk, alice);
t.assert(tokens.length == 3);
let initial_try = await mint_raw(tenk, alice, cost);
t.assert(initial_try.failed);
// owner can still mint
const second_token = await mint_raw(tenk, root);
t.assert(second_token.succeeded);

await sleepTimer;
await root.call(tenk, "end_premint", {
base_cost: ONE_NEAR,
min_cost: ONE_NEAR,
await root.call(tenk, "add_whitelist_accounts", {
accounts: [alice],
allowance: 2,
});
await mint(tenk, alice, cost);
await mint(tenk, alice, cost);
let last_try = await mint_raw(tenk, alice, cost);
t.assert(last_try.failed);
const tokens = await getTokens(tenk, alice);
t.assert(tokens.length == 3);
});
const sale_price = await totalCost(tenk, 1, alice.accountId);
t.assert(sale_price.gt(cost), "actual sale price has increased");

await mint(tenk, alice, sale_price);
t.assert((await mint_raw(tenk, alice, sale_price)).failed);
t.assert((await mint_raw(tenk, root, sale_price)).succeeded);
});
1 change: 0 additions & 1 deletion contracts/tenk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,4 @@ rand_xorshift = "0.2.0"
[features]
default = []
mainnet = []
on_sale = []
airdrop = []
90 changes: 58 additions & 32 deletions contracts/tenk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pub struct Contract {
is_premint: bool,
is_premint_over: bool,
premint_deadline_at: u64,
allowance: Option<u32>,
}
const DEFAULT_SUPPLY_FATOR_NUMERATOR: u8 = 20;
const DEFAULT_SUPPLY_FATOR_DENOMENTOR: Balance = 100;
Expand Down Expand Up @@ -110,6 +111,7 @@ impl Contract {
initial_royalties: Option<Royalties>,
is_premint: Option<bool>,
is_premint_over: Option<bool>,
allowance: Option<u32>,
) -> Self {
royalties.as_ref().map(|r| r.validate());
initial_royalties.as_ref().map(|r| r.validate());
Expand All @@ -133,6 +135,7 @@ impl Contract {
is_premint.unwrap_or(false),
is_premint_over.unwrap_or(false),
0,
allowance,
)
}

Expand All @@ -149,6 +152,7 @@ impl Contract {
is_premint: bool,
is_premint_over: bool,
premint_deadline_at: u64,
allowance: Option<u32>,
) -> Self {
metadata.assert_valid();
Self {
Expand All @@ -175,12 +179,20 @@ impl Contract {
is_premint,
is_premint_over,
premint_deadline_at,
allowance,
}
}

pub fn add_whitelist_account(&mut self, account_id: AccountId, allowance: u32) {
pub fn add_whitelist_accounts(&mut self, accounts: Vec<AccountId>, allowance: Option<u32>) {
self.assert_owner();
self.whitelist.insert(&account_id, &allowance);
require!(
accounts.len() <= 10,
"Can't add more than ten accounts at a time"
);
let allowance = allowance.unwrap_or(self.allowance.unwrap_or(0));
accounts.iter().for_each(|account_id| {
self.whitelist.insert(account_id, &allowance);
});
}

pub fn whitelisted(&self, account_id: AccountId) -> bool {
Expand All @@ -201,7 +213,6 @@ impl Contract {
);
self.is_premint = true;
self.premint_deadline_at = env::block_height() + duration;
log!("New deadline {}", self.premint_deadline_at);
}

pub fn end_premint(&mut self, base_cost: U128, min_cost: U128, percent_off: Option<u8>) {
Expand All @@ -211,7 +222,10 @@ impl Contract {
self.is_premint_over == false,
"premint has already been done"
);
require!(self.premint_deadline_at < env::block_height() , "premint is still in process");
require!(
self.premint_deadline_at < env::block_height(),
"premint is still in process"
);
self.is_premint = false;
self.is_premint_over = true;
self.percent_off = percent_off.unwrap_or(0);
Expand All @@ -237,11 +251,9 @@ impl Contract {
let total_cost = self.cost_of_linkdrop(account).0;
self.pending_tokens += 1;
let mint_for_free = self.is_owner(account);
self.use_whitelist_allowance(account, 1);
log!("Total cost of creation is {}", total_cost);
refund(account, deposit - total_cost);
if self.is_premint {
self.use_whitelist_allowance(account, 1);
}
self.send(public_key, mint_for_free)
.then(ext_self::on_send_with_callback(
env::current_account_id(),
Expand Down Expand Up @@ -282,9 +294,7 @@ impl Contract {
let owner_id = &env::signer_account_id();
let num = self.assert_can_mint(owner_id, num);
let tokens = self.nft_mint_many_ungaurded(num, owner_id, false);
if self.is_premint {
self.use_whitelist_allowance(owner_id, num);
}
self.use_whitelist_allowance(owner_id, num);
tokens
}

Expand Down Expand Up @@ -358,6 +368,10 @@ impl Contract {
self.metadata.get().unwrap()
}

pub fn remaining_allowance(&self, account_id: &AccountId) -> u32 {
self.whitelist.get(account_id).unwrap_or(0)
}

// Owner private methods

pub fn transfer_ownership(&mut self, new_owner: AccountId) {
Expand All @@ -375,6 +389,11 @@ impl Contract {
self.royalties.replace(&royalties)
}

pub fn update_allowance(&mut self, allowance: u32) {
self.assert_owner();
self.allowance = Some(allowance);
}

// Contract private methods

#[private]
Expand All @@ -401,33 +420,29 @@ impl Contract {
}

// Private methods
fn assert_deposit(&self, num: u32) {
fn assert_deposit(&self, num: u32, account_id: &AccountId) {
require!(
env::attached_deposit() >= self.total_cost(num, &env::signer_account_id()).0,
env::attached_deposit() >= self.total_cost(num, account_id).0,
"Not enough attached deposit to buy"
);
}

fn assert_can_mint(&self, account_id: &AccountId, num: u32) -> u32 {
fn assert_can_mint(&mut self, account_id: &AccountId, num: u32) -> u32 {
let mut num = num;
// Check quantity
// Owner can mint for free
if !self.is_owner(account_id) {
if self.is_premint {
let allowance = self.get_whitelist_allowance(&account_id);
num = u32::min(allowance, num);
require!(num > 0, "Account has no more allowance in the whitelist");
let allowance = if self.is_premint {
self.get_whitelist_allowance(&account_id)
} else {
require!(self.is_premint_over, "Premint period must be over");
}
self.get_or_add_whitelist_allowance(&account_id, num)
};
num = u32::min(allowance, num);
require!(num > 0, "Account has no more allowance left");
}
require!(self.tokens_left() >= num, "No NFTs left to mint");

if on_sale() {
self.assert_deposit(num);
} else {
env::panic_str("Minting is not available")
};
self.assert_deposit(num, account_id);
num
}

Expand Down Expand Up @@ -489,16 +504,31 @@ impl Contract {
}

fn use_whitelist_allowance(&mut self, account_id: &AccountId, num: u32) {
let allowance = self.get_whitelist_allowance(account_id);
let new_allowance = allowance - num;
self.whitelist.insert(&account_id, &new_allowance);
if self.has_allowance() && !self.is_owner(account_id) {
let allowance = self.get_whitelist_allowance(account_id);
let new_allowance = allowance - u32::min(num, allowance);
self.whitelist.insert(&account_id, &new_allowance);
}
}

fn get_whitelist_allowance(&self, account_id: &AccountId) -> u32 {
self.whitelist
.get(account_id)
.unwrap_or_else(|| panic!("Account not on whitelist"))
}

fn get_or_add_whitelist_allowance(&mut self, account_id: &AccountId, num: u32) -> u32 {
// return num if allowance isn't set
self.allowance.map_or(num, |allowance| {
self.whitelist.get(account_id).unwrap_or_else(|| {
self.whitelist.insert(&account_id, &allowance);
allowance
})
})
}
fn has_allowance(&self) -> bool {
self.allowance.is_some() || self.is_premint
}
}

near_contract_standards::impl_non_fungible_token_core!(Contract, tokens);
Expand All @@ -508,11 +538,6 @@ near_contract_standards::impl_non_fungible_token_enumeration!(Contract, tokens);
fn log_mint(owner_id: &str, token_ids: Vec<String>) {
NearEvent::log_nft_mint(owner_id.to_string(), token_ids, None);
}

fn on_sale() -> bool {
cfg!(feature = "on_sale")
}

const fn to_near(num: u32) -> Balance {
(num as Balance * 10u128.pow(24)) as Balance
}
Expand Down Expand Up @@ -545,6 +570,7 @@ mod tests {
None,
None,
None,
None,
)
}

Expand Down
3 changes: 2 additions & 1 deletion contracts/tenk/src/linkdrop.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::*;
use near_sdk::{
env, ext_contract, json_types::U128, near_bindgen, AccountId, Balance, Gas, Promise, PublicKey, log,
env, ext_contract, json_types::U128, log, near_bindgen, AccountId, Balance, Gas, Promise,
PublicKey,
};
use near_units::parse_near;

Expand Down
Loading

0 comments on commit 1f57f61

Please sign in to comment.