Skip to content

Commit

Permalink
Implemented marketplace branch with updated SDK version and updated r…
Browse files Browse the repository at this point in the history
…ust toolchain
  • Loading branch information
BenKurrek committed Dec 6, 2021
1 parent 7e51665 commit d316401
Show file tree
Hide file tree
Showing 12 changed files with 1,358 additions and 1 deletion.
590 changes: 590 additions & 0 deletions market-contract/Cargo.lock

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions market-contract/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "nft_simple"
version = "0.1.0"
authors = ["Near Inc <[email protected]>"]
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
near-sdk = "=4.0.0-pre.4"

[profile.release]
codegen-units=1
opt-level = "z"
lto = true
debug = false
panic = "abort"
overflow-checks = true
1 change: 1 addition & 0 deletions market-contract/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# TBD
6 changes: 6 additions & 0 deletions market-contract/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash
set -e

RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release
mkdir -p ../out
cp target/wasm32-unknown-unknown/release/*.wasm ../out/market.wasm
Binary file added market-contract/res/nft_simple.wasm
Binary file not shown.
23 changes: 23 additions & 0 deletions market-contract/src/external.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use crate::*;

/// external contract calls
//initiate a cross contract call to the nft contract. This will transfer the token to the buyer and return
//a payout object used for the market to distribute funds to the appropriate accounts.
#[ext_contract(ext_contract)]
trait ExtContract {
fn nft_transfer_payout(
&mut self,
receiver_id: AccountId, //purchaser (person to transfer the NFT to)
token_id: TokenId, //token ID to transfer
approval_id: u64, //market contract's approval ID in order to transfer the token on behalf of the owner
memo: String, //memo (to include some context)
/*
the price that the token was purchased for. This will be used in conjunction with the royalty percentages
for the token in order to determine how much money should go to which account.
*/
balance: U128,
//the maximum amount of accounts the market can payout at once (this is limited by GAS)
max_len_payout: u32,
);
}
58 changes: 58 additions & 0 deletions market-contract/src/internal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use crate::*;

//used to generate a unique prefix in our storage collections (this is to avoid data collisions)
pub(crate) fn hash_account_id(account_id: &AccountId) -> CryptoHash {
//get the default hash
let mut hash = CryptoHash::default();
//we hash the account ID and return it
hash.copy_from_slice(&env::sha256(account_id.as_bytes()));
hash
}

impl Contract {
//internal method for removing a sale from the market. This returns the previously removed sale object
pub(crate) fn internal_remove_sale(
&mut self,
nft_contract_id: AccountId,
token_id: TokenId,
) -> Sale {
//get the unique sale ID (contract + DELIMITER + token ID)
let contract_and_token_id = format!("{}{}{}", &nft_contract_id, DELIMETER, token_id);
//get the sale object by removing the unique sale ID. If there was no sale, panic
let sale = self.sales.remove(&contract_and_token_id).expect("No sale");

//get the set of sales for the sale's owner. If there's no sale, panic.
let mut by_owner_id = self.by_owner_id.get(&sale.owner_id).expect("No sale by_owner_id");
//remove the unique sale ID from the set of sales
by_owner_id.remove(&contract_and_token_id);

//if the set of sales is now empty after removing the unique sale ID, we simply remove that owner from the map
if by_owner_id.is_empty() {
self.by_owner_id.remove(&sale.owner_id);
//if the set of sales is not empty after removing, we insert the set back into the map for the owner
} else {
self.by_owner_id.insert(&sale.owner_id, &by_owner_id);
}

//get the set of token IDs for sale for the nft contract ID. If there's no sale, panic.
let mut by_nft_contract_id = self
.by_nft_contract_id
.get(&nft_contract_id)
.expect("No sale by nft_contract_id");

//remove the token ID from the set
by_nft_contract_id.remove(&token_id);

//if the set is now empty after removing the token ID, we remove that nft contract ID from the map
if by_nft_contract_id.is_empty() {
self.by_nft_contract_id.remove(&nft_contract_id);
//if the set is not empty after removing, we insert the set back into the map for the nft contract ID
} else {
self.by_nft_contract_id
.insert(&nft_contract_id, &by_nft_contract_id);
}

//return the sale object
sale
}
}
180 changes: 180 additions & 0 deletions market-contract/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::collections::{LookupMap, UnorderedMap, UnorderedSet};
use near_sdk::json_types::{U128, U64};
use near_sdk::serde::{Deserialize, Serialize};
use near_sdk::{
assert_one_yocto, env, ext_contract, near_bindgen, AccountId, Balance, Gas, PanicOnDefault,
Promise, CryptoHash, BorshStorageKey,
};
use std::collections::HashMap;

use crate::external::*;
use crate::internal::*;
use crate::sale::*;
use near_sdk::env::STORAGE_PRICE_PER_BYTE;

mod external;
mod internal;
mod nft_callbacks;
mod sale;
mod sale_views;

//GAS constants to attach to calls
const GAS_FOR_ROYALTIES: Gas = Gas(115_000_000_000_000);
const GAS_FOR_NFT_TRANSFER: Gas = Gas(15_000_000_000_000);

//constant used to attach 0 NEAR to a call
const NO_DEPOSIT: Balance = 0;

//the minimum storage to have a sale on the contract.
const STORAGE_PER_SALE: u128 = 1000 * STORAGE_PRICE_PER_BYTE;

//every sale will have a unique ID which is `CONTRACT + DELIMITER + TOKEN_ID`
static DELIMETER: &str = ".";

//Creating custom types to use within the contract. This makes things more readable.
pub type SalePriceInNear = U128;
pub type TokenId = String;
pub type FungibleTokenId = AccountId;
pub type ContractAndTokenId = String;
pub type Payout = HashMap<AccountId, U128>;


//main contract struct to store all the information
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Contract {
//keep track of the owner of the contract
pub owner_id: AccountId,

/*
to keep track of the sales, we map the ContractAndTokenId to a Sale.
the ContractAndTokenId is the unique identifier for every sale. It is made
up of the `contract ID + DELIMITER + token ID`
*/
pub sales: UnorderedMap<ContractAndTokenId, Sale>,

//keep track of all the Sale IDs for every account ID
pub by_owner_id: LookupMap<AccountId, UnorderedSet<ContractAndTokenId>>,

//keep track of all the token IDs for sale for a given contract
pub by_nft_contract_id: LookupMap<AccountId, UnorderedSet<TokenId>>,

//keep track of the storage that accounts have payed
pub storage_deposits: LookupMap<AccountId, Balance>,
}

/// Helper structure to for keys of the persistent collections.
#[derive(BorshStorageKey, BorshSerialize)]
pub enum StorageKey {
Sales,
ByOwnerId,
ByOwnerIdInner { account_id_hash: CryptoHash },
ByNFTContractId,
ByNFTContractIdInner { account_id_hash: CryptoHash },
ByNFTTokenType,
ByNFTTokenTypeInner { token_type_hash: CryptoHash },
FTTokenIds,
StorageDeposits,
}

#[near_bindgen]
impl Contract {
/*
initialization function (can only be called once).
this initializes the contract with default data and the owner ID
that's passed in
*/
#[init]
pub fn new(owner_id: AccountId) -> Self {
let this = Self {
//set the owner_id field equal to the passed in owner_id.
owner_id,

//Storage keys are simply the prefixes used for the collections. This helps avoid data collision
sales: UnorderedMap::new(StorageKey::Sales),
by_owner_id: LookupMap::new(StorageKey::ByOwnerId),
by_nft_contract_id: LookupMap::new(StorageKey::ByNFTContractId),
storage_deposits: LookupMap::new(StorageKey::StorageDeposits),
};

//return the Contract object
this
}

//Allows users to deposit storage. This is to cover the cost of storing sale objects on the contract
//Optional account ID is to users can pay for storage for other people.
#[payable]
pub fn storage_deposit(&mut self, account_id: Option<AccountId>) {
//get the account ID to pay for storage for
let storage_account_id = account_id
//convert the valid account ID into an account ID
.map(|a| a.into())
//if we didn't specify an account ID, we simply use the caller of the function
.unwrap_or_else(env::predecessor_account_id);

//get the deposit value which is how much the user wants to add to their storage
let deposit = env::attached_deposit();

//make sure the deposit is greater than or equal to the minimum storage for a sale
assert!(
deposit >= STORAGE_PER_SALE,
"Requires minimum deposit of {}",
STORAGE_PER_SALE
);

//get the balance of the account (if the account isn't in the map we default to a balance of 0)
let mut balance: u128 = self.storage_deposits.get(&storage_account_id).unwrap_or(0);
//add the deposit to their balance
balance += deposit;
//insert the balance back into the map for that account ID
self.storage_deposits.insert(&storage_account_id, &balance);
}

//Allows users to withdraw any excess storage that they're not using. Say Bob pays 0.01N for 1 sale
//Alice then buys Bob's token. This means bob has paid 0.01N for a sale that's no longer on the marketplace
//Bob could then withdraw this 0.01N back into his account.
#[payable]
pub fn storage_withdraw(&mut self) {
//make sure the user attaches exactly 1 yoctoNEAR for security purposes.
//this will redirect them to the NEAR wallet (or requires a full access key).
assert_one_yocto();

//the account to withdraw storage to is always the function caller
let owner_id = env::predecessor_account_id();
//get the amount that the user has by removing them from the map. If they're not in the map, default to 0
let mut amount = self.storage_deposits.remove(&owner_id).unwrap_or(0);

//how many sales is that user taking up currently. This returns a set
let sales = self.by_owner_id.get(&owner_id);
//get the length of that set.
let len = sales.map(|s| s.len()).unwrap_or_default();
//how much NEAR is being used up for all the current sales on the account
let diff = u128::from(len) * STORAGE_PER_SALE;

//the excess to withdraw is the total storage paid - storage being used up.
amount -= diff;

//if that excess to withdraw is > 0, we transfer the amount to the user.
if amount > 0 {
Promise::new(owner_id.clone()).transfer(amount);
}
//we need to add back the storage being used up into the map if it's greater than 0.
//this is so that if the user had 500 sales on the market, we insert that value here so
//if those sales get taken down, the user can then go and withdraw 500 sales worth of storage.
if diff > 0 {
self.storage_deposits.insert(&owner_id, &diff);
}
}

/// views
//return the minimum storage for 1 sale
pub fn storage_minimum_balance(&self) -> U128 {
U128(STORAGE_PER_SALE)
}

//return how much storage an account has paid for
pub fn storage_balance_of(&self, account_id: AccountId) -> U128 {
U128(self.storage_deposits.get(&account_id).unwrap_or(0))
}
}
Loading

0 comments on commit d316401

Please sign in to comment.