Select Page

How to Develop an NFT Marketplace on Algorand?

dApp on Algorand

A recent announcement by FIFA took the blockchain community by storm: its partnership with blockchain innovator Algorand. The blockchain protocol Algorand is to aid FIFA in developing a “digital assets’ strategy” and provide an official “wallet solution.”

It is not the first time Algorand has partnered with a major project. Previously, the federal government of Marshall Islands announced collaborating with Algorand to create its first national currency, SOV, which is also the world’s first national currency. Likewise, there have been several other partnership announcements like the FIDE Grand Prix series (a world chess tournament), SIAE (an Italian company for collective copyright management), Republic (an investing platform and technology services) and the list goes on. All such announcements of partnerships prove that Algorand has become a reliable blockchain ecosystem in the blockchain space.

Algorand was launched in 2017 by a team led by Silvio Micali, a Turing Award recipient recognized for his contributions to cryptography in the year 2012. He is the co-creator of various cryptographic technologies like verifiable random proofs, zero-knowledge proofs etc.

Micali believed that blockchain protocols could achieve decentralization, scalability and security simultaneously without sacrificing any of them, as proposed by Vitalik Buterin in the year 2016, the founder of Ethereum. This inspired Micali to create Algorand, a permissionless blockchain designed to solve the blockchain trilemma that has kept the blockchain community perplexed for years. It is a smart contract-oriented platform that provides instant transaction finality without forking. Algo, being the native cryptocurrency of Algorand, is used for staking, governance and transactions.

This article offers an insight into Algorand and elaborates on the steps to develop an NFT marketplace on Algorand.

About Algorand

Algorand is a blockchain ecosystem that functions on a unique consensus mechanism known as Pure Proof-of-Stake that enables it to process up to 1000 transactions and provides immediate transaction finality without forking. Pure Proof-of-Stake has two types of nodes: participant nodes and relay nodes. As the name suggests, participant nodes participate in consensus while relay nodes enable interaction between the participant nodes. In Pure Proof-of-Stake, 1000 participant nodes are randomly chosen to produce a block, from which one is randomly selected to add its block to the Algorand blockchain. Being selected depends on how much Algo a participant node has staked. The higher the number of Algos staked, the better the chances of getting selected.

The Algorand network comprises three key features:

  • The Algorand Standard Assets – It is a layer 1 feature permitting users to represent any asset on the Algorand blockchain while benefiting from the security, compatibility, speed and ease of use as the Algo.
  • Algorand Atomic Transfer – These are irreducible batch transactions that guarantee the simultaneous execution of multiple transactions of any asset.
  • Smart Contracts – Algorand offers highly customizable smart contracts built directly on the layer-1 blockchain.

 

Algorand smart contracts can be categorized as:

  • Stateful smart contracts: They can store and update on-chain values and implement application logic.
  • Stateless smart contracts: They are frequently used to approve spending or asset transfers, and they govern all transactions from a single account.

Now that we have an idea about the Algorand infrastructure, let us go through Algorand NFT marketplace development.

How to develop an NFT Marketplace on Algorand?

Install the packages mentioned below in your Python environment:

  • PyYAML
  • py_algorand_sdk
  • pyteal
  • streamlit

In addition, create a “config.yml” file where you have to add your credentials to connect with the Algorand client.

accounts:
account_1:
address: PUBLIC_KEY_VALUE
mnemonic: MNEMONIC_WORDS
private_key: PRIVATE_KEY_VALUE
account_2:
address: PUBLIC_KEY_VALUE
mnemonic: MNEMONIC_WORDS
private_key: PRIVATE_KEY_VALUE
account_3:
address: PUBLIC_KEY_VALUE
mnemonic: MNEMONIC_WORDS
private_key: PRIVATE_KEY_VALUE
total: 3
client_credentials:
address: ADDRESS_VALUE
token: TOKEN_VALUE

Step 1: Define the Stateful Smart Contract Interface

A stateful smart contract represents an application living on the Algorand network. Application call transactions facilitate the interaction between the users and the application. Presently, transactions are created and sent over the network by a user-interface application, a mobile app, a web app, or CLI.

To begin, outline an “NFTMarketplaceInterface” that involves all required methods. These methods describe the interaction between the external applications and the stateful smart contracts.

class NFTMarketplaceInterface(ABC):
    @abstractmethod
    def initialize_escrow(self, escrow_address):
        pass

    @abstractmethod
    def make_sell_offer(self, sell_price):
        pass

    @abstractmethod
    def buy(self):
        pass

    @abstractmethod
    def stop_sell_offer(self):
        pass

An abstract python class is used to represent the interface. All of the methods described in this interface must be implemented by the smart contract that wants to comply with it. It is crucial to note that this interface can be used by several smart contracts, each of which can have different implementation logic. The external communication will not change despite the separate implementation logic.

In the “NFTMarketplaceInterface”, four methods are defined:

  • “initialize_escrow(escrow_address)” – This initializes the escrow address used to transfer the NFT.
  • “make_sell_offer(sell_price)” – It generates a sell offer for the NFT at a specific “sell_price”.
  • “buy()” – A user purchases the NFT from the sell offer
  • “stop_sell_offer()” – Cancels the current NFT selling offer.

Step 2: Implement Stateful Smart Contract

Next, we have to construct a concrete class called “NFTMarketplaceASC1” that executes the “NFTMarketplaceInterface.” This class will store all of the PyTeal logic for the stateful smart contract.

Defining the constants

Every stateful smart contract can have global and local states, forming the applications state data. Key-value pairs are used to represent these states. Defining the keys as constants is a good practice that makes the code easier to read and less prone to errors. Besides, a schema also specifies the number of integers and bytes used in key-value pairs. There is a global schema for the global state and a local schema for the local state.

class NFTMarketplaceASC1(NFTMarketplaceInterface):
    class Variables:
        escrow_address = Bytes("ESCROW_ADDRESS")
        asa_id = Bytes("ASA_ID")
        asa_price = Bytes("ASA_PRICE")
        asa_owner = Bytes("ASA_OWNER")
        app_state = Bytes("APP_STATE")
        app_admin = Bytes("APP_ADMIN")

    class AppMethods:
        initialize_escrow = "initializeEscrow"
        make_sell_offer = "makeSellOffer"
        buy = "buy"
        stop_sell_offer = "stopSellOffer"

    class AppState:
        not_initialized = Int(0)
        active = Int(1)
        selling_in_progress = Int(2)

    @property
    def global_schema(self):
        return algosdk.future.transaction.StateSchema(num_uints=3,
                                                      num_byte_slices=3)

    @property
    def local_schema(self):
        return algosdk.future.transaction.StateSchema(num_uints=0,
                                                      num_byte_slices=0)

We shall break the constants into various classes to make the code more readable and maintainable:

  • “Variables” – Specifies the keys for the smart contract’s global variables. Those variables represent the application’s global state.
  • “AppMethods” – Specifies all the methods that allow the smart contract to communicate with external applications. Every constant in this class must be mapped to a distinct method defined in a specific interface.

Variables

The contract consists of six global variables, three integers, and three bytes.

  • “escrow_address” – Includes the address of the stateless smart contract that is in charge of the Algorand Standard Assets (ASA) transfer, i.e., NFT.
  • “asa_id” – This is the NFT’s unique identifier. The smart contract is in charge of buying and selling the NFT corresponding to this id.
  • “asa_price” – When a sell offer is active, this is the price of the NFT.
  • “asa_owner” – Contains the present owner’s address of the NFT. Only this address can make a sell offer for the NFT.
  • “app_admin” – It holds the admin’s address. Only this address can set up the “escrow_address” via a call to the “initialize_escrow” method.
  • “app_state” – It represents one of the AppState class’s potential application states.
    The following are the three probable outcomes:
  • “not_initialized” – The smart contract is launched on the network, but the escrow has not yet been initialized. Only the “initialize_escrow” method can be used when the contract is in this condition.
  • “active” – The contract has been linked to the “asa_owner” and the NFT. However, the NFT is still not for sale, indicating that the owner has yet to offer it for sale.
  • “selling_in_progress” – The NFT has been put up for sale by the owner. A buyer can use the “buy” method to purchase the NFT when the application is in this state.

Methods of application

The “NFTMarketplaceInterface,” which has four methods, is implemented by the stateful smart contract. As a result, an “AppMethods” class is created, which uniquely identifies those methods:

  • “initialize_escrow” – Configures the smart contract’s “escrow_address”.
    According to the logic, only an application call made by the “app_admin” address can invoke the method. Furthermore, the “escrow_address” can only be initialized once.
  • “make_sell_offer” – To put the NFT on sale for a set price of “asa_price” micro algos, the “asa_owner” address can utilize this method.
  • “buy” – Once the NFT is on sale, a purchaser can use this method to buy the NFT.
  • “stop_sell _offer” – The “asa_owner” can use this method to end the current sale offer for the NFT.

Initialization of the application

We need three required arguments to deploy the application on the network:

  • The address that presently owns the NFT
  • The address that has admin rights in the application
  • The NFT’s ID

The “asa_owner” and “app_admin” are sent as application parameters, while the “asa_id” is transferred to the “foreign_assets” field of the “ApplicationCreateTxn.”

def app_initialization(self):
     return Seq([
         Assert(Txn.application_args.length() == Int(2)),
         App.globalPut(self.Variables.app_state, self.AppState.not_initialized),
         App.globalPut(self.Variables.asa_id, Txn.assets[0]),
         App.globalPut(self.Variables.asa_owner, Txn.application_args[0]),
         App.globalPut(self.Variables.app_admin, Txn.application_args[1]),
         Return(Int(1))
     ])

Initialize escrow

After the application is successfully deployed, the admin must first call the “initialize_escrow” method to set up the stateless contract responsible for sending the NFT. We accomplish this by making the address of the stateless smart contract the NFT’s clawback address.

We want to ensure that all of the NFT’s properties are as expected when interacting with this method.

  • The NFT should not have a management address. With this condition, we eliminate the option of modifying the NFT’s clawback address. Because we use and store the clawback address in the stateful smart contract, we want it to be consistent.
  • We check whether the NFT’s clawback address is the same as the escrow address we deliver as an argument.
  • The NFT should be frozen, or else the NFT might be sent to any address while the “asa_owner” state variable remains unchanged.
 def initialize_escrow(self, escrow_address):
        curr_escrow_address = App.globalGetEx(Int(0), self.Variables.escrow_address) 

        asset_escrow = AssetParam.clawback(Txn.assets[0])
        manager_address = AssetParam.manager(Txn.assets[0])
        freeze_address = AssetParam.freeze(Txn.assets[0])
        reserve_address = AssetParam.reserve(Txn.assets[0])
        default_frozen = AssetParam.defaultFrozen(Txn.assets[0])

        return Seq([
            curr_escrow_address,
            Assert(curr_escrow_address.hasValue() == Int(0)), 

            Assert(App.globalGet(self.Variables.app_admin) == Txn.sender()),
            Assert(Global.group_size() == Int(1)),

            asset_escrow,
            manager_address,
            freeze_address,
            reserve_address,
            default_frozen,
            Assert(Txn.assets[0] == App.globalGet(self.Variables.asa_id)),
            Assert(asset_escrow.value() == Txn.application_args[1]),
            Assert(default_frozen.value()),
            Assert(manager_address.value() == Global.zero_address()),
            Assert(freeze_address.value() == Global.zero_address()),
            Assert(reserve_address.value() == Global.zero_address()),

            App.globalPut(self.Variables.escrow_address, escrow_address),
            App.globalPut(self.Variables.app_state, self.AppState.active),
            Return(Int(1))
        ])

The application switches to an “AppState.active” state after the admin has successfully initialized the escrow. The “asa_owner” will now be allowed to make NFT sell bids.

Make a purchase offer.

The NFT is sold through sell offers and the application can only be accessed by the NFT owner to initiate a sell offer. The method name and the NFT price are involved as arguments in the application call transaction. The internal state of this application will be changed from “AppState.active” to “AppState.selling_in_progress” with this application call.

    def make_sell_offer(self, sell_price):
        valid_number_of_transactions = Global.group_size() == Int(1)
        app_is_active = Or(App.globalGet(self.Variables.app_state) == self.AppState.active,
                           App.globalGet(self.Variables.app_state) == self.AppState.selling_in_progress)

        valid_seller = Txn.sender() == App.globalGet(self.Variables.asa_owner)
        valid_number_of_arguments = Txn.application_args.length() == Int(2)

        can_sell = And(valid_number_of_transactions,
                       app_is_active,
                       valid_seller,
                       valid_number_of_arguments)

        update_state = Seq([
            App.globalPut(self.Variables.asa_price, Btoi(sell_price)),
            App.globalPut(self.Variables.app_state, self.AppState.selling_in_progress),
            Return(Int(1))
        ])

        return If(can_sell).Then(update_state).Else(Return(Int(0)))

Buy

The application’s “buy” function is the most difficult to use. This part of the code is executed by an application call transaction that is combined into an Atomic Transfer that has three transactions:

  1. Application call transaction performs the PyTeal code contained within the “buy” function. There we verify if the application is in the “AppState.selling_in _progress” state, the “asa_owner” receives “asa_price” algos, while the “asa_buyer” receives the NFT.
  2. A payment transaction that pays the vendor of the NFT the correct quantity of algos.
  3. The NFT is transferred from the current owner to the new owner via an atomic transfer transaction. The sender of the prior two Atomic Transfer transactions is now the new owner.
 def buy(self):
        valid_number_of_transactions = Global.group_size() == Int(3)
        asa_is_on_sale = App.globalGet(self.Variables.app_state) == self.AppState.selling_in_progress

        valid_payment_to_seller = And(
            Gtxn[1].type_enum() == TxnType.Payment,
            Gtxn[1].receiver() == App.globalGet(self.Variables.asa_owner), # correct receiver
            Gtxn[1].amount() == App.globalGet(self.Variables.asa_price), # correct amount 
            Gtxn[1].sender() == Gtxn[0].sender(), # equal sender of the first two transactions, which is the buyer
            Gtxn[1].sender() == Gtxn[2].asset_receiver() # correct receiver of the NFT
        )

        valid_asa_transfer_from_escrow_to_buyer = And(
            Gtxn[2].type_enum() == TxnType.AssetTransfer,
            Gtxn[2].sender() == App.globalGet(self.Variables.escrow_address),
            Gtxn[2].xfer_asset() == App.globalGet(self.Variables.asa_id),
            Gtxn[2].asset_amount() == Int(1)
        )

        can_buy = And(valid_number_of_transactions,
                      asa_is_on_sale,
                      valid_payment_to_seller,
                      valid_asa_transfer_from_escrow_to_buyer)

        update_state = Seq([
            App.globalPut(self.Variables.asa_owner, Gtxn[0].sender()), # update the owner of the ASA.
            App.globalPut(self.Variables.app_state, self.AppState.active), # update the app state
            Return(Int(1))
        ])

        return If(can_buy).Then(update_state).Else(Return(Int(0)))

Offer to stop selling

We want to add the ability to cancel a selling order as a final feature to the application. If the “asa_owner” wants to cease selling the NFT, they can do so via an Application call transaction that implements the “stop_selling_offer” method. This is a rather straightforward function in which the application’s internal state is updated to “AppState.active” from “AppState.selling_in_progress” only if the application’s sender is the NFT owner.

def stop_sell_offer(self):
        valid_number_of_transactions = Global.group_size() == Int(1)
        valid_caller = Txn.sender() == App.globalGet(self.Variables.asa_owner)
        app_is_initialized = App.globalGet(self.Variables.app_state) != self.AppState.not_initialized

        can_stop_selling = And(valid_number_of_transactions,
                               valid_caller,
                               app_is_initialized)

        update_state = Seq([
            App.globalPut(self.Variables.app_state, self.AppState.active),
            Return(Int(1))
        ])

        return If(can_stop_selling).Then(update_state).Else(Return(Int(0)))

Step 3: Implement Stateless Smart Contract

As previously stated, we will employ a stateless smart contract as the NFT’s clawback address. Once the code in the contract is evaluated, we can move the NFT from one account to another.

Because the stateful smart contract contains the majority of the functionality for the NFTMarketplace application, the escrow contract has to meet the following requirements:

  • The contract signs the AssetTransfer transaction, part of an Atomic Transfer. When transferring NFT from one address to another, the only way is to use the application’s “buy” method.
  • We need to double-check that the Atomic Transfer’s first transaction calls the correct application. We pass the id from the stateful smart contract as “app_id” when compiling the code in the escrow contract.
  • With the “asa_id” parameter, we check if we are transferring the correct NFT. The escrow address for each NFT will be different.
def nft_escrow(app_id: int, asa_id: int):
    return Seq([
        Assert(Global.group_size() == Int(3)), # atomic transfer with three transactions
        Assert(Gtxn[0].application_id() == Int(app_id)), # we are calling the right application

        Assert(Gtxn[1].type_enum() == TxnType.Payment),

        Assert(Gtxn[2].asset_amount() == Int(1)),
        Assert(Gtxn[2].xfer_asset() == Int(asa_id)), # we are transferring the correct NFT
        Assert(Gtxn[2].fee() <= Int(1000)),
        Assert(Gtxn[2].asset_close_to() == Global.zero_address()),
        Assert(Gtxn[2].rekey_to() == Global.zero_address()),

        Return(Int(1))
    ])

We finish all of the PyTeal code in the NFTMarketplace application with the escrow stateless smart contract. The Algorand blockchain will be used to run this code. The only thing left for us to accomplish now is to put the communication with the contracts into action.

Step 4: Communication Services

Transactions are how we communicate with smart contracts. Separating the creation of transactions into different functions is a smart approach. In addition, we will divide the logical functions into distinct classes, which we will refer to as services.

We have two types of services in our app:

  • All interactions with the stateful smart contract are implemented via the “NFTMarketplace” service. We can use this service to invoke the methods implemented in the “NFTMarketplaceInterface.” In addition, we have another way that deploys the application to the blockchain.
  • The following methods are available in the “NFTMarketplace”:
    • “app_initialization”
    • “initialize_escrow”
    • “fund_escrow”
    • “make_sell_offer”
    • “buy_nft”

    We don’t utilize the “stop_sell_offer” method in the application’s UI; hence, we did not include it in the service class. Implementing the necessary transaction to execute that method is a desirable exercise.

  • “NFTService” allows us to build an NFT, modify its credentials, and add a user to it.
    So, the “NFTService” consists of the following methods:
    • “create_nft”
    • “change_nft_credentials”
    • “opt_in”

NFT Marketplace

Each “NFTMarketplace” service instance will represent a separate stateful smart contract that has been put on the network. Each of those contracts is in charge of overseeing the state of a single NFT.

The following code is used to set up the smart contract:

class NFTMarketplace:
    def __init__(self, admin_pk, admin_address, nft_id, client):
        self.admin_pk = admin_pk
        self.admin_address = admin_address
        self.nft_id = nft_id

        self.client = client

        self.teal_version = 4
        self.nft_marketplace_asc1 = NFTMarketplaceASC1()

        self.app_id = None

We initialize the “NFTMarketplaceASC1” stateful smart contract in the constructor method of the class, in addition to the appropriate arguments. The TEAL code submitted to the network will be obtained from this item.

The first function we define produces and performs the network transaction that submits the stateful smart contract.

We can obtain this with the following code:

def app_initialization(self, nft_owner_address):
        approval_program_compiled = compileTeal(
            self.nft_marketplace_asc1.approval_program(),
            mode=Mode.Application,
            version=4,
        )

        clear_program_compiled = compileTeal(
            self.nft_marketplace_asc1.clear_program(),
            mode=Mode.Application,
            version=4
        )

        approval_program_bytes = NetworkInteraction.compile_program(
            client=self.client, source_code=approval_program_compiled
        )

        clear_program_bytes = NetworkInteraction.compile_program(
            client=self.client, source_code=clear_program_compiled
        )

        app_args = [
            decode_address(nft_owner_address),
            decode_address(self.admin_address),
        ]

        app_transaction = ApplicationTransactionRepository.create_application(
            client=self.client,
            creator_private_key=self.admin_pk,
            approval_program=approval_program_bytes,
            clear_program=clear_program_bytes,
            global_schema=self.nft_marketplace_asc1.global_schema,
            local_schema=self.nft_marketplace_asc1.local_schema,
            app_args=app_args,
            foreign_assets=[self.nft_id],
        )

        tx_id = NetworkInteraction.submit_transaction(
            self.client, transaction=app_transaction
        )

        transaction_response = self.client.pending_transaction_info(tx_id)

        self.app_id = transaction_response["application-index"]

        return tx_id

The function above has the objective of defining an Application Create Transaction and submitting it to the network. This transaction can accept various parameters, so we need to specify the ones we will utilize in our application. The “app_initialization” method of the “NFTMarketplaceASC1” contract defined the need for the specific parameters.

The following stages summarize the implementation logic:

  • From the stateful smart contract, we acquire and compile the clear and the approval program.
  • Generate an “app_args” array that stores the “nft_owner_address” and the “admin_address”. When the application creates the transaction, this array will be sent to the “apps_args.”
  • Transfer the “nft_id” in the application to create the transaction’s foreign_assets field parameter.

If this transaction completes successfully, we have succeeded in deploying the application to manage the selling and re-selling of NFTs with the specific “nft_id.”

The “nft_id” and the “app_id,” the necessary parameters to initialize the escrow address, are now available. We will use this address as the clawback address in the NFT later.

We can acquire the escrow address and the escrow bytes using the “nft_escrow” function:

    @property
    def escrow_bytes(self):
        if self.app_id is None:
            raise ValueError("App not deployed")

        escrow_fund_program_compiled = compileTeal(
            nft_escrow(app_id=self.app_id, asa_id=self.nft_id),
            mode=Mode.Signature,
            version=4,
        )

        return NetworkInteraction.compile_program(
            client=self.client, source_code=escrow_fund_program_compiled
        )

    @property
    def escrow_address(self):
        return algo_logic.address(self.escrow_bytes)

We now need to execute the “initialize_escrow” function that will implement the corresponding transaction, which will, in turn, set up the escrow address in the stateful smart contract. Remember that we must have modified the NFT’s management credentials before interacting with this function. In the “NFTService,” the code to modify the NFT’s administration shall be explained.

def initialize_escrow(self):
        app_args = [
            self.nft_marketplace_asc1.AppMethods.initialize_escrow,
            decode_address(self.escrow_address),
        ]

        initialize_escrow_txn = ApplicationTransactionRepository.call_application(
            client=self.client,
            caller_private_key=self.admin_pk,
            app_id=self.app_id,
            on_complete=algo_txn.OnComplete.NoOpOC,
            app_args=app_args,
            foreign_assets=[self.nft_id],
        )

        tx_id = NetworkInteraction.submit_transaction(
            self.client, transaction=initialize_escrow_txn
        )

        return tx_id

The beginning state of the stateful smart contract is updated to “AppState.active” after successful submission of the “initialize_escrow” transaction. We have to carry out the “make_sell_offer” and “buy_nft” methods.

The “make_sell_offer” is merely a normal application call transaction where we transfer two arguments: the method name and the sell offer price. When the transaction sender has the necessary NFT, the stateful smart contract will confirm the transaction.

def make_sell_offer(self, sell_price: int, nft_owner_pk):
        app_args = [self.nft_marketplace_asc1.AppMethods.make_sell_offer, sell_price]

        app_call_txn = ApplicationTransactionRepository.call_application(
            client=self.client,
            caller_private_key=nft_owner_pk,
            app_id=self.app_id,
            on_complete=algo_txn.OnComplete.NoOpOC,
            app_args=app_args,
            sign_transaction=True,
        )

        tx_id = NetworkInteraction.submit_transaction(self.client, transaction=app_call_txn)
        return tx_id

As is customary, we must enforce the most complex method. To purchase an NFT, we have to submit three Atomic Transfer transactions:

  1. Application call transaction carried out by the buyer to the application call. We only send the method name as an application argument, which is relatively simple.
  2. The buyer does a payment transaction to the seller of the NFT. This transaction’s value should be the same as the amount specified in the sale offer.
  3. Atomic transfer transaction to the purchaser’s address from the escrow address. The escrow address is also the NFT’s clawback address, and the NFT can be transferred from one address to another. This transaction has to be signed with a Logic Signature. Once the TEAL logic in the escrow contract is evaluated to be true, the transaction is approved.

The “buy_nft” function, which produces all required transactions to purchase the NFT, is described in the code below.

def buy_nft(self, nft_owner_address, buyer_address, buyer_pk, buy_price):
        # 1. Application call txn
        app_args = [
            self.nft_marketplace_asc1.AppMethods.buy
        ]

        app_call_txn = ApplicationTransactionRepository.call_application(client=self.client,
                                                                         caller_private_key=buyer_pk,
                                                                         app_id=self.app_id,
                                                                         on_complete=algo_txn.OnComplete.NoOpOC,
                                                                         app_args=app_args,
                                                                         sign_transaction=False)

        # 2. Payment transaction: buyer -> seller
        asa_buy_payment_txn = PaymentTransactionRepository.payment(client=self.client,
                                                                   sender_address=buyer_address,
                                                                   receiver_address=nft_owner_address,
                                                                   amount=buy_price,
                                                                   sender_private_key=None,
                                                                   sign_transaction=False)

        # 3. Asset transfer transaction: escrow -> buyer

        asa_transfer_txn = ASATransactionRepository.asa_transfer(client=self.client,
                                                                 sender_address=self.escrow_address,
                                                                 receiver_address=buyer_address,
                                                                 amount=1,
                                                                 asa_id=self.nft_id,
                                                                 revocation_target=nft_owner_address,
                                                                 sender_private_key=None,
                                                                 sign_transaction=False)

        # Atomic transfer
        gid = algo_txn.calculate_group_id([app_call_txn,
                                           asa_buy_payment_txn,
                                           asa_transfer_txn])

        app_call_txn.group = gid
        asa_buy_payment_txn.group = gid
        asa_transfer_txn.group = gid

        app_call_txn_signed = app_call_txn.sign(buyer_pk)

        asa_buy_txn_signed = asa_buy_payment_txn.sign(buyer_pk)

        asa_transfer_txn_logic_signature = algo_txn.LogicSig(self.escrow_bytes)
        asa_transfer_txn_signed = algo_txn.LogicSigTransaction(asa_transfer_txn, asa_transfer_txn_logic_signature)

        signed_group = [app_call_txn_signed,
                        asa_buy_txn_signed,
                        asa_transfer_txn_signed]

        tx_id = self.client.send_transactions(signed_group)
        return tx_id

The “NFTMarketplace” service is now complete with the execution of this method.

NFTService

Each instance of the “NFTService” class will be in charge of a single NFT on the blockchain. The necessary arguments for generating an NFT like the creator’s address, NFT name and an optional URL that holds an ipfs image need to be sent to the initializer.

class NFTService:
    def __init__(
            self,
            nft_creator_address: str,
            nft_creator_pk: str,
            client,
            unit_name: str,
            asset_name: str,
            nft_url=None,
    ):
        self.nft_creator_address = nft_creator_address
        self.nft_creator_pk = nft_creator_pk
        self.client = client

        self.unit_name = unit_name
        self.asset_name = asset_name
        self.nft_url = nft_url

        self.nft_id = None

An Asset Config Transaction is used to generate the NFT. Every management field of the NFT is filled at this point which we can change later. If we leave the NFT with those management credentials, the stateful smart contract should dismiss it.

def create_nft(self):
        signed_txn = ASATransactionRepository.create_non_fungible_asa(
            client=self.client,
            creator_private_key=self.nft_creator_pk,
            unit_name=self.unit_name,
            asset_name=self.asset_name,
            note=None,
            manager_address=self.nft_creator_address,
            reserve_address=self.nft_creator_address,
            freeze_address=self.nft_creator_address,
            clawback_address=self.nft_creator_address,
            url=self.nft_url,
            default_frozen=True,
            sign_transaction=True,
        )

        nft_id, tx_id = NetworkInteraction.submit_asa_creation(
            client=self.client, transaction=signed_txn
        )
        self.nft_id = nft_id
        return tx_id

The “change_nft_credentials_txn” method is implemented, which clears all of the NFT’s credentials except the escrow address.

def change_nft_credentials_txn(self, escrow_address):
        txn = ASATransactionRepository.change_asa_management(
            client=self.client,
            current_manager_pk=self.nft_creator_pk,
            asa_id=self.nft_id,
            manager_address="",
            reserve_address="",
            freeze_address="",
            strict_empty_address_check=False,
            clawback_address=escrow_address,
            sign_transaction=True,
        )

        tx_id = NetworkInteraction.submit_transaction(self.client, transaction=txn)

        return tx_id

Finally, we must execute an easy method that permits users to choose a specific NFT. This is a great property of the Algorand network that prevents users from receiving tokens which have not been authorized to be stored in their wallets. The user can obtain an Algorand Standard Asset once successfully opting in.

 def opt_in(self, account_pk):
        opt_in_txn = ASATransactionRepository.asa_opt_in(
            client=self.client, sender_private_key=account_pk, asa_id=self.nft_id
        )

        tx_id = NetworkInteraction.submit_transaction(self.client, transaction=opt_in_txn)
        return tx_id

Step 5: Deployment of Algorand TestNet

Finally, we have executed everything. Thanks to the services, we can now deploy and test the NFTMarketplace application with ease on the Algorand TestNet. The script below replicates making a sell offer and implementing it by a particular buyer. From the following code, we can notice how everything is abstracted well, making the transaction execution convenient.

nft_service.create_nft()

nft_marketplace_service.app_initialization(nft_owner_address=admin_addr)

nft_service.change_nft_credentials_txn(escrow_address=nft_marketplace_service.escrow_address)

nft_marketplace_service.initialize_escrow()
nft_marketplace_service.fund_escrow()
nft_marketplace_service.make_sell_offer(sell_price=100000, nft_owner_pk=admin_pk)

nft_service.opt_in(buyer_pk)

nft_marketplace_service.buy_nft(nft_owner_address=admin_addr,
                                buyer_address=buyer_addr,
                                buyer_pk=buyer_pk,
                                buy_price=100000)

Conclusion

Non-Fungible Tokens are gaining popularity day by day, and there is increased traffic to existing NFT marketplaces like Axie Infinity, Rarible and OpenSea. Big firms find this new wave of technology a profitable opportunity to invest in and capitalize on for business growth, using the chance to its fullest potential. With immense business growth possibilities, there arises the demand for NFT marketplaces powered by robust blockchain ecosystems.

Although many blockchain platforms are available, Algorand is one of the finest and most reliable for NFT marketplace development. It was launched to solve the blockchain trilemma of simultaneously achieving scalability, security and decentralization. Being one of the fastest networks, it can finalize transactions in less than five seconds and is also a cost-effective protocol. Its distinctive Pure Proof-of-Stake consensus mechanism, robust security, guaranteed finality, global scale reach, broad decentralization, and low transaction cost make it one of the best platforms for developing and deploying a standard NFT marketplace.

If you want to build a custom NFT marketplace, consult our NFT development experts, who will guide you in your marketplace platform’s development and launch.

Webinar Details

Author’s Bio

Akash Takyar
Akash Takyar
CEO LeewayHertz
Akash Takyar is the founder and CEO at LeewayHertz. The experience of building over 100+ platforms for startups and enterprises allows Akash to rapidly architect and design solutions that are scalable and beautiful.
Akash's ability to build enterprise-grade technology solutions has attracted over 30 Fortune 500 companies, including Siemens, 3M, P&G and Hershey’s. Akash is an early adopter of new technology, a passionate technology enthusiast, and an investor in AI and IoT startups.

Start a conversation by filling the form

Once you let us know your requirement, our technical expert will schedule a call and discuss your idea in detail post sign of an NDA.
All information will be kept confidential.

Insights

Follow Us