Skip to content

NFT Auction dAPP where one user can create an nft, and have other users bid for it. The highest bidder will gain ownership of the NFT and be able to resell it again.

Notifications You must be signed in to change notification settings

Joseph-Gicuguma/NFT-Auction

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

31 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

NFT Auction

In this tutorial, we'll walk through a simple decentralized application, the NFT Auction dAPP; where one user can create an nft, and have other users bid for it. The highest bidder will gain ownership of the NFT and be able to resell it again.

This tutorial aims to give the required knowledge to build, test, and implement custom blockchain logic easily.

Getting Started

Installation and Initialization

Reach is designed to work on POSIX systems with make, Docker, and Docker Compose installed. The best way to install Docker on Mac and Windows is with Docker Desktop.

You probably already have make installed. For example, OS X and many other POSIX systems come with make, but some versions of Linux do not include it by default and will require you to install it. If you’re on Ubuntu, you can run sudo apt install make to get it.

You’ll know that you have everything installed if you can run the following three commands without errors

$ make --version

$ docker --version

$ docker-compose --version

If you’re using Windows, consult the guide to using Reach on Windows.

Once you’ve confirmed that they are installed, choose a directory for this project. We recommend

$ mkdir -p ~/reach/nftroyalties && cd ~/reach/nftroyalties

Next, install Reach by downloading it from GitHub by running

$ curl https://raw.githubusercontent.com/reach-sh/reach-lang/master/reach -o reach ; chmod +x reach

You’ll know that the download worked if you can run

$ ./reach version

Since Reach is Dockerized, when you first use it, you’ll need to download the images it uses. This will happen automatically when you first use it, but you can do it manually now by running

$ ./reach update

You’ll know that everything is in order if you can run

$ ./reach compile --help

Get language support for Reach in your editor by visiting IDE/Text Editor Support. Now that your Reach installation is in order, you should open a text editor and get ready to write your Reach application!

Scaffolding and Setup

In this tutorial, we’ll be building a version of NFT Auction! There will be a creator who owns Nft and starts the auction, and two people who bid on the auction. We’ll start simple and slowly make the application more fully-featured.

You should follow along by copying each part of the program and seeing how things go. If you’re like us, you may find it beneficial to type each line out, rather than copying & pasting so you can start building your muscle memory and begin to get a sense for each part of a Reach program

Tutorial

Below are the steps to help the reader re-create the same application application. We assume that you already know the basics of reach. If not, checkout the Rock Paper Scissors Tutorial

By the end of this tutorial you will be able to create a D-App where one user can create an nft, and have other users bid for it. The highest bidder will gain ownership of the NFT and be able to resell it again; in a buy sell cycle.

Test First But Verify

Rather than jumping into the Reach program, we're first going to write a test scenario corresponding to the bidding process. We'll demonstrate how to use Reach's testing tools to write a convenient testing framework customized for your application. We'll show the tests, then the framework, then the Reach code, and show how the Reach code connects to the framework.

After setting up your project with ./reach init. Clear index.mjs and index.rsh

In index.mjs,

NFT-Auction/src/index.mjs

Lines 103 to 112 in d77732e

test.one('NFTEST', async () => {
const NFT = await makeNft({
creatorLabel: 'Creator',
getId: 100,
nftDetails: {
basePrice: stdlib.parseCurrency(10),
uri: '12345678'
},
deadline: 50,
});

In this sample, we use test.one to define a single test scenario. We use the function makeNft, which we will define later, to create a JavaScript object for the NFT abstraction. When it is created, it has the details of the event in it.

NFT-Auction/src/index.mjs

Lines 114 to 118 in d77732e

const Creator = NFT.Creator;
const Bidders = await NFT.makeBidders([
'Bidder1', 'Bidder2', 'Bidder3'
]);
const [Bidder1, Bidder2, Bidder3] = Bidders;

Next, we define objects for each of the people involved in the scenario. This code uses NFT.makeBidders, a function which we will define later, to turn a list of labels into Bidder abstractions.

NFT-Auction/src/index.mjs

Lines 120 to 146 in 9f74443

await Bidder1.placeBid(stdlib.parseCurrency(25));
await test.chkErr('Creator', 'The auction is already going on', async () => {
await Creator.startAuction();
});
await Bidder3.placeBid(stdlib.parseCurrency(30));
await test.chkErr('Bidder1', 'Need to bid a higher price', async () => {
await Bidder1.placeBid(stdlib.parseCurrency(30));
});
await test.chkErr('Bidder3', 'Already placed bid', async () => {
await Bidder3.placeBid(stdlib.parseCurrency(40));
});
await Bidder2.placeBid(stdlib.parseCurrency(35));
await Bidder3.placeBid(stdlib.parseCurrency(40));
await NFT.waitUntilDeadline();
await test.chkErr('Creator', 'Not the nft owner', async () => {
await Creator.startAuction();
});
await test.chkErr('Bidder1', 'Nft not for sale', async () => {
await Bidder1.placeBid(stdlib.parseCurrency(30));
});
await Bidder3.startAuction();
await Bidder1.placeBid(stdlib.parseCurrency(25));
await Bidder2.placeBid(stdlib.parseCurrency(35));
await test.chkErr('Bidder3', 'Cannot buy your nft', async () => {
await Bidder3.placeBid(stdlib.parseCurrency(40));
});
await NFT.waitUntilDeadline();

From then on we perform the bidding cycle. test.chkErr Is to confirm various checks that might through error as will be seen in the reach code. await NFT.waitUntilDeadline(); will be used to make sure the bidding sesssion terminates.

NFT-Auction/src/index.mjs

Lines 148 to 154 in 9f74443

for (const p of [Creator, ...Bidders]) {
const bal = await p.getBalance();
console.log(`${p.label} has ${bal}`)
}
});
await test.run({ noVarOutput: true });

Finally, we print out the balances of everyone and see that they match our expectations. The function test.run instructs Reach to run all of the tests and not print out extra debugging information.

Framework Implementation

The framework is needed to facilitate the testing. It needs to provide:

  • makeNft => A function which accepts the details of an nft and returns an NFT abstraction.
  • NFT.Creator => An abstraction of the Host.
  • NFT.makeBidders => A function that produces an array of Bidder abstractions, which are subclasses of Person abstractions.
  • NFT.waitUntilDeadline() => A function that waits until the deadline has passed.
  • Person.startAuction() => A function for a person to start an auction
  • Bidder.placeBid() => A function for one bidder to place a bid
  • Person.getBalance => A function to read one person's balance.

import { loadStdlib, test } from '@reach-sh/stdlib';
import * as backend from './build/index.main.mjs';
// Basics
const stdlib = loadStdlib({ REACH_NO_WARN: 'Y' });

First, we have the basic header that imports and initializes the Reach standard library.

//Framework
const makeNft = async({creatorLabel, getId, nftDetails, deadline : timeLimit}) => {
const sbal = stdlib.parseCurrency(100);
const accCreator = await stdlib.newTestAccount(sbal);
accCreator.setDebugLabel(creatorLabel);

We define the makeRSVP function and create an initial test account for the host and set its label for debugging.

const stdPerson = (obj) => {
const { acc } = obj;
const getBalance = async () => {
const bal = await acc.balanceOf();
return `${stdlib.formatCurrency(bal, 4)} ${stdlib.standardUnit}`;
};
return {
...obj,
getBalance,
};
};
const Creator = stdPerson({
acc: accCreator,
label: creatorLabel,
startAuction: (ctc) => {},
});

Next, we define the function stdPerson which takes an obj with an acc field and adds a Person.getBalance function that returns the account's current balance as a nice formatted string. We use this function to define the NFT.Creator value

});
const waitUntilDeadline = async () => {
const deadline = (await stdlib.getNetworkTime()).add(timeLimit * 3);
console.log(`Waiting until ${deadline}`);
await stdlib.waitUntilTime(deadline);
};

Next, we define the deadline, based on the current time, and the NFT.waitUntilDeadline function for waiting until that time has passed.

const details = nftDetails;
const ctcCreator = accCreator.contract(backend);
ctcCreator.participants.Creator(
{
createNFT : () => {
return details
},
getId,
deadline : timeLimit
}
);
const ctcInfo = ctcCreator.getInfo();
console.log(`${creatorLabel} launched contract`);
ctcCreator.e.isAuctionOn.monitor((event) => {
const {when, what : [auctionOn]} = event
console.log(`Auction on is ${auctionOn}`)
});
ctcCreator.e.showBid.monitor((event) => {
const {when, what : [_who, bid]} = event
const who = stdlib.formatAddress(_who);
console.log(`${creatorLabel} sees ${who} places a bid of ${bid}`)
})
ctcCreator.e.seeOutcome.monitor((event) => {
const {when, what : [_who, bid]} = event
const who = stdlib.formatAddress(_who);
console.log(`${creatorLabel} sees ${who} bought the nft at a price of ${bid}`)
})

Now, we can define the details object that will be consumed by Reach then pass the object interact object for the creator contract. There after we have the creator observe event so that we can be notified on various actions.

const makeBidder = async (label) => {
const acc = await stdlib.newTestAccount(sbal);
acc.setDebugLabel(label);
const ctcBidder = acc.contract(backend, ctcInfo);
const placeBid = async (bid) => {
await ctcBidder.a.Bidder.getBid(bid)
console.log(`${label} placed a bid of ${bid}`)
}
const startAuction = async () => {
console.log("Called")
await ctcBidder.a.Owner.isAuctionOn(true);
console.log(`${label} starting the auction`);
}

Next, we define the NFT.makeBidder function, which starts by creating a new test account and setting its label. There after we define the bidder functions.

return stdPerson({
acc, label, placeBid, startAuction
});
}
const makeBidders = (labels) => Promise.all(labels.map(makeBidder));
Creator.startAuction = async () => {
console.log("Called")
await ctcCreator.a.Owner.isAuctionOn(true);
console.log(`${creatorLabel} starting the auction`);
}
return {Creator, makeBidder, makeBidders, waitUntilDeadline};
};

We close the definition of the Bidder abstraction by calling stdPerson to add the Person.getBalance function. Then, we define NFT.makeBidders, which produces a single promise out of the array of promises of Bidder abstractions. Also we define Creator.startAuction. These values are all wrapped together into a final object, which is the result of makeNFT.

Views and Events

First, we'll review the changes to the Reach application code.

export const main = Reach.App(() => {
const Creator = Participant('Creator', {
getId: UInt,
createNFT: Fun([], Details),
deadline: UInt,
});
const Owner = API('Owner', {
isAuctionOn: Fun([Bool], Null)
});
const Bidder = API('Bidder', {
getBid: Fun([UInt], Null),
});
const Info = View('Info', {
details : Details,
owner : Address
});
const Notify = Events({
seeOutcome: [Address, UInt],
showBid: [Address, UInt],
isAuctionOn: [Bool],
});
init();

We add definitions for the View and Event objects.

Let's look at the View first. The first argument is a label for it, like how we give labels to APIs and participants. Next, we provide an object where the keys are the names of the view components and the fields are their types. This object is just like an interact object, except that the values are provided from Reach, rather than to Reach. In this case, like APIs, these values can be accessed on- and off-chain. On-chain, they can be accessed using the normal ABI of the consensus network, just like APIs. For example, the details are provided via a function named Info_details that takes no arguments and returns a Details structure. Off-chain, they can be accessed via a frontend function like ctc.views.Info.details(). The off-chain function returns the value or an indication that it was not available.

Next, let's look at the Events definition. It can also be provided with a label, but we've chosen not to include one. We don't have to provide labels for APIs or Views either, but we think it is a good idea in those cases. The object provided to Events is not an interface, where the keys are types, but instead has tuples of types as the values. These are the values that will be emitted together. For example, the seeOutcome event will contain an address and an integer. Like APIs and Views, they are available on- and off-chain. On-chain, they are available using the standard ABI for the platform. (Although, note, that some chains, like Ethereum, don't provide any on-chain mechanism for consuming events.) Off-chain, they are available via a frontend function like ctc.events.register. The off-chain function has sub-methods for reading the next instance of the event or monitoring every event, as well as other options.

In both cases, we have not actually defined the values or meaning of these Views and Events. We've merely indicated that our application contains them. This is similar to how we define a Participant and then later indicate what actions it performs. Let's look at the view definitions next.

Creator.only(() => {
const id = declassify(interact.getId);
const deadline = declassify(interact.deadline);
const nftInfo = declassify(interact.createNFT());
});
Creator.publish(id, deadline, nftInfo);
Notify.isAuctionOn(true);
Info.details.set(nftInfo);

A View can have a different value at each point in the program, including not having any value at all. You define the value by calling <View name>.<field name>.set and providing a value that satisfies the type. For example, here (on line 43) we indicate that the details field is always the same as the nftInfo variable. This definition applies to all dominated occurrences of the commit() keyword. Views are not mutable references: instead, they are ways of naming, for external consumption, portions of the consensus state.

parallelReduce([ Creator, nftInfo.basePrice, Creator, true, true, true])
.define(()=>{
Info.owner.set(owner);
})

We similarly expose the contents of the Guests mapping, as well as the owner variable. We use the .define feature of parllelReduce to introduce a statement that dominates the commit()s implicit in the parallelReduce. This context is the only context that has access to the owner variable, which is why we must place it there.

Next, let's look at the code that emits instances of the Events we defined.

.api_(Owner.isAuctionOn, (enteredIsAuctionOn) => {
check(this === owner, "Not the nft owner");
check(auctionOn === false, "The auction is already going on");
return [0, (ret)=>{
Notify.isAuctionOn(enteredIsAuctionOn);
ret(null);
return [owner, price, lastBidder, keepGoing, enteredIsAuctionOn, firstBid];
}]
})

We can emit an event by calling <Events name>.<kind name>(args) in a consensus step. We do so inside of the .api_ for the Owner.isAuctionOn API call on line 62. Note that in each of the ._api calls, various checks are inplace to ensure that they can be called from the frontend only under specific cercirmastances.

.timeout(relativeTime(deadline), () => {
Creator.publish();
if(lastBidder !== owner) {
Notify.seeOutcome(lastBidder, price);
}
Notify.isAuctionOn(false);
transfer(firstBid ? 0 : price).to(owner);
return [lastBidder, nftInfo.basePrice, lastBidder, true, false, true];
});

There are many other instances where Events are emmit in the contract. As seen above in lines 81-84 , we wrap it inside an if statement to ensure that the event is emmitted only when the NFT ownership has changed.

In conslusion, from the perspective of a frontend or even a conventionaly application, the View serve the purpose of providing general and primary information for setting up the applications while Events are there to ensure the application of updated live with actions taken from other users that might affect the current user.

Having a Frontend

This sections assume you are well firmiliar with React framework but will be explained ensure that it can be replicated on other frameworks.

We use the MainAppContext.js to store general application State and its modifier methods. The Bidder.js and Creator.js which extend Participant.js are provided as interact object that enable the user to interact with the frontend.

setUpObservables () {
const {account, setLatestOutcome, setLatestBid} = this.context;
this.contract.e.isAuctionOn.monitor((event) => {
const {when, what : [auctionOn]} = event
console.log(`Auction on is ${auctionOn}`)
this.setState({
isAuctionOn : auctionOn,
args : [this.nftInfo.uri, this.contractInfo, this.navigateToAuction, this.nftInfo.uri]
})
});
this.contract.e.showBid.monitor((event) => {
const {when, what : [_who, _bid]} = event
const who = Reach.formatAddress(_who);
const bid = fmt(_bid);
console.log(`You see ${who} places a bid of ${bid}`)
setLatestBid({
who,
bid
})
})
this.contract.e.seeOutcome.monitor((event) => {
const {when, what : [_who, _bid]} = event
const who = Reach.formatAddress(_who);
const bid = fmt(_bid);
console.log(`You see ${who} bought the nft at a price of ${bid}`)
setLatestOutcome({
who,
bid
})
setLatestBid({});
this.setState({
appState: "seeOutcome",
isOwner : account.networkAccount.addr == who,
// isOwner : account.getAddress() == _who,
args : [this.nftInfo.uri, , this.navigateToAuction]
})
})
}

As seen above, for all users, this.contract.e.<event name>.monitor is used to keep the frontend update with events on-chain.

async setIsAuctionOn () {
await this.contract.a.Owner.isAuctionOn(true);
}
async placeBid (bid) {
await this.contract.a.Bidder.getBid(Reach.parseCurrency(bid));
}

The this.contract.a.<Api name>.<Method name>() is used to make calls to the API methods to place bids by the users.

async createNFT () {
return this.nftInfo
}

The createNft method serves it corresponding function for the Creator Particiapant.

Other than those, the rest in Bidder.js and Creator.js are concerned with statemanagement, navigation and deployment of the code which are out of the scope of thise tutorial.

About

NFT Auction dAPP where one user can create an nft, and have other users bid for it. The highest bidder will gain ownership of the NFT and be able to resell it again.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published