A PoC backbone for NFT Marketplaces on NEAR Protocol.
- basic purchase of NFT with FT
- demo pay out royalties (FTs and NEAR)
- test and determine standards for markets (best practice?) to buy/sell NFTs (finish standard) with FTs (already standard)
- demo some basic auction types, secondary markets and
- frontend example
- first pass / internal audit
- connect with bridged tokens e.g. buy and sell with wETH/nDAI (or whatever we call these)
- approve NFT on marketplace A and B
- it sells on B
- still listed on A
- user Alice goes to purchase on marketplace A but this will fail
- the token has been transferred already and marketplace A has an incorrect approval ID for this NFT
There are 3 potential solutions:
- handle in market contract - When it fails because nft_transfer fails, marketplace could make promise call that checks nft_token owner_id is still sale owner_id and remove sale. This means it will still fail for 1 user.
- handle with backend - run a cron job to check sales on a regular interval. This potentially avoids failing for any user.
- remove from frontend (use frontend or backend) - for every sale, check that sale.owner_id == nft.owner_id and then hide these sale options in the frontend UI. Heavy processing for client side, so still needs a backend.
- let it fail client side then alert backend to remove sale. No cron. Still fails for 1 user.
Matt's opinion: Option (2/3) is the best UX and also allows your sale listings to be the most accurate and up to date. If you're implementing a marketplace, you most likely are running backend somewhere with the marketplace owner account. If you go with Option 3 you can simply update a list of "invalid sales" and filter these before you send the sales listings to the client. If you decided to go with 2, modify the marketplace remove_sale to allow your marketplace owner account to remove any sales.
High level diagram of NFT sale on Market using Fungible Token:
Differences from nft-simple
NFT standard reference implementation:
- anyone can mint an NFT
- Optional token_type
- capped supply by token_type
- lock transfers by token_token
- enumerable.rs
Frontend App Demo: /src/
- install dependencies and test
yarn && yarn test:deploy
- run app -
yarn start
App Tests: /test/app.test.js
- install, deploy, test
yarn && yarn test:deploy
- if you update contracts -
yarn test:deploy
- if you update tests only -
yarn test
Associated Video Demos (most recent at top)
Older Walkthrough Videos:
Some additional ideas around user onboarding:
Install Rust https://rustup.rs/
- Install near-cli:
npm i -g near-cli
- Create testnet account: Wallet
- Login:
near login
- Install everything:
yarn
- Deploy the contract and run the app tests:
yarn test:deploy
- If you ONLY change the code in the JS tests, use
yarn test
. - If you change the contract run
yarn test:deploy
again. - If you run out of funds in the dev account run
yarn test:deploy
again. - If you change the dev account (yarn test:deploy) the server should restart automatically, but you may need to restart the app and sign out/in again with NEAR Wallet.
There's 3 main areas to explore:
- contracts - shows how to create a simple NFT marketplace where you can mint tokens and put them for sale so they can be bought.
- frontend app - shows how to create a simple NFT marketplace where you can mint tokens and put them for sale so they can be bought.
- app.test.js - code that is used to test different interactions with the smart contracts.
The tests are set up to auto generate the dev account each time you run test:deploy
e.g. you will get a new NFT contract address each time you run a test.
This is just for testing. You can obviously deploy a token to a fixed address on testnet / mainnet, it's an easy config update.
There is only one config.js file found in src/config.js
, this is also used for running tests.
Using src/config.js
you can set up your different environments. Use REACT_APP_ENV
to switch environments e.g. in package.json
script deploy
.
There are helpers in test/test-utils.js
that take care of:
- creating a near connection and establishing a keystore for the dev account
- creating test accounts each time a test is run
- establishing a contract instance so you can call methods
You can change the default funding amount for test accounts in src/config.js
In src/state/near.js
you will see that src/config.js
is loaded as a function. This is to satisfy the jest/node test runner.
You can destructure any properies of the config easily in any module you import it in like this:
// example file app.js
import getConfig from '../config';
export const {
GAS,
networkId, nodeUrl, walletUrl, nameSuffix,
contractName,
} = getConfig();
Note the export const in the destructuring?
Now you can import these like so:
//example file Component.js
import { GAS } from '../app.js'
...
await contract.withdraw({ amount: parseNearAmount('1') }, GAS)
...
- Bundled with Parcel 2.0 (@next) && eslint
- Minimal all-in-one state management with async/await support
The following steps describe how to use
src/utils/state
to create and use your ownstore
andStateProvider
.
- Create a file e.g.
/state/app.js
and add the following code
import { State } from '../utils/state';
// example
const initialState = {
app: {
mounted: false
}
};
export const { store, Provider } = State(initialState);
- Now in your
index.js
wrap yourApp
component with theStateProvider
import { Provider } from './state/app';
ReactDOM.render(
<Provider>
<App />
</Provider>,
document.getElementById('root')
);
- Finally in
App.js
you canuseContext(store)
const { state, dispatch, update } = useContext(store);
<p>Hello {state.foo && state.foo.bar.hello}</p>
const handleClick = () => {
update('clicked', !state.clicked);
};
const onMount = () => {
dispatch(onAppMount('world'));
};
useEffect(onMount, []);
When a function is called using dispatch, it expects arguments passed in to the outer function and the inner function returned to be async with the following json args: { update, getState, dispatch }
Example of a call:
dispatch(onAppMount('world'));
All dispatched methods and update calls are async and can be awaited. It also doesn't matter what file/module the functions are in, since the json args provide all the context needed for updates to state.
For example:
import { helloWorld } from './hello';
export const onAppMount = (message) => async ({ update, getState, dispatch }) => {
update('app', { mounted: true });
update('clicked', false);
update('data', { mounted: true });
await update('', { data: { mounted: false } });
console.log('getState', getState());
update('foo.bar', { hello: true });
update('foo.bar', { hello: false, goodbye: true });
update('foo', { bar: { hello: true, goodbye: false } });
update('foo.bar.goodbye', true);
await new Promise((resolve) => setTimeout(() => {
console.log('getState', getState());
resolve();
}, 2000));
dispatch(helloWorld(message));
};
The default names the State
factory method returns are store
and Provider
. However, if you want multiple stores and provider contexts you can pass an additional prefix
argument to disambiguate.
export const { appStore, AppProvider } = State(initialState, 'app');
The updating of a single store, even several levels down, is quite quick. If you're worried about components re-rendering, use memo
:
import React, { memo } from 'react';
const HelloMessage = memo(({ message }) => {
console.log('rendered message');
return <p>Hello { message }</p>;
});
export default HelloMessage;
Higher up the component hierarchy you might have:
const App = () => {
const { state, dispatch, update } = useContext(appStore);
...
const handleClick = () => {
update('clicked', !state.clicked);
};
return (
<div className="root">
<HelloMessage message={state.foo && state.foo.bar.hello} />
<p>clicked: {JSON.stringify(state.clicked)}</p>
<button onClick={handleClick}>Click Me</button>
</div>
);
};
When the button is clicked, the component HelloMessage will not re-render, it's value has been memoized (cached). Using this method you can easily prevent performance intensive state updates in further down components until they are neccessary.
Reference: