forked from near-examples/nft-tutorial
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implemented marketplace branch with updated SDK version and updated r…
…ust toolchain
- Loading branch information
Showing
12 changed files
with
1,358 additions
and
1 deletion.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# TBD |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} |
Oops, something went wrong.