Marketplace Contract Part 1 - Asset for Algo

Video Walkthrough

Watch the tutorial video for this chapter:

Step 1: Generate Two Accounts

  • One for Lister, one for Buyer
from algosdk.account import generate_account

private_key, address = generate_account()
print(private_key, address)

Step 2: Load each Account with Funds

Use the following link to load funds into each account:

https://bank.testnet.algorand.network/

Step 3: Create .env

algod_token = 
algod_server = https://testnet-api.4160.nodely.dev
private_key = lqPuVOtF4p6QpsaL7iTA6mbHoy7a5+nuAPjzw7HvckiXQWUlnZQVYSCZSvUK8w+MGjh09sNsx7WybOeb0MLD/Q==
buyer_private_key = jHx+aE6G/5RKAMJK7J7R+j3SyjKhzLsrbYIrzmmhWphJsT7CpTxqdhsjWQx4wDpx2I5B+1FiBSp16O5fXd/Sqg==
asset_1 = 724395141
asset_2 = 724449087
app_id = 724455779

Step 4: Compile Asset for Algo Listing Contract

Create and compile the asset listing contract:

algokit compile py fileName.py

Use the following implementation to compile your contract:

from algopy import ARC4Contract, String, gtxn, Asset, Txn, GlobalState, itxn, Global, BoxRef, UInt64, TransactionType
from algopy import ARC4Contract, String, gtxn, Asset, Txn, GlobalState, itxn, Global, BoxRef, UInt64, TransactionType
from algopy.arc4 import abimethod, Address, Struct, Bool
from algopy.arc4 import UInt64 as arc4UInt64

class listingName(Struct):
    lister: Address
    counter: arc4UInt64    
    
class listingValue(Struct):
    asset_listed: arc4UInt64
    algo_requested: arc4UInt64    
    
class listings(ARC4Contract):
    def __init__(self) -> None:
        self.listingCounter = GlobalState(arc4UInt64(0))    
    
    @abimethod
    def triggerOptIn(
        self,
        asset_to_opt_into: Asset,
        fee_payment: gtxn.PaymentTransaction
    ) -> tuple[String, UInt64]:

        assert fee_payment.amount >= 101_000
        assert fee_payment.receiver == Global.current_application_address
        
        itxn.AssetTransfer(
            xfer_asset=asset_to_opt_into,
            asset_receiver=Global.current_application_address,
            fee=Global.min_txn_fee
        ).submit()    
    
        return String("Successfully opted in to asset: "), asset_to_opt_into.id    
    
    @abimethod
    def postListingRequest(
        self,
        asset_to_list: gtxn.AssetTransferTransaction,
        algo_amount_requested: arc4UInt64,
        box_fee_mbr_payment: gtxn.PaymentTransaction,
    ) -> tuple[String, UInt64, String, arc4UInt64]:
        
        assert box_fee_mbr_payment.amount == 24_900
        assert asset_to_list.asset_receiver == Global.current_application_address
        assert asset_to_list.asset_amount == 1
        
        self.listingCounter.value = arc4UInt64(self.listingCounter.value.native + 1)
        
        listing_box_name_info = listingName(Address(Txn.sender), self.listingCounter.value)
        
        listing_box = BoxRef(key=listing_box_name_info.bytes)
        
        value, exists = listing_box.maybe()
        assert not exists
        
        listing_box.create(size=16)
        
        listing_box_value_info = listingValue(arc4UInt64(asset_to_list.xfer_asset.id), algo_amount_requested)
        
        listing_box.put(listing_box_value_info.bytes)
        
        return String("User Listed Asset Successfully: "), asset_to_list.xfer_asset.id, String("User is requesting x amount of Algo: "), algo_amount_requested    
    
    @abimethod
    def fulfillListingRequest(
        self,
        asset_listed: Asset,
        listing_name: listingName,
        payment: gtxn.PaymentTransaction,
        transfer_fees: gtxn.PaymentTransaction
    ) -> tuple[String, UInt64, String, UInt64]:
        
        assert transfer_fees.amount == 2000
        
        listing_box = BoxRef(key=listing_name.bytes)
        
        value, exists = listing_box.maybe()
        
        assert exists
        
        listing_value = listingValue.from_bytes(value)
        
        assert listing_value.asset_listed == asset_listed.id
        assert listing_value.algo_requested == payment.amount
        
        itxn.Payment(
            receiver=listing_name.lister.native,
            amount=payment.amount,
            fee=Global.min_txn_fee
        ).submit()
        
        itxn.AssetTransfer(
            xfer_asset=asset_listed,
            asset_receiver=Txn.sender,
            asset_amount=1,
            fee=Global.min_txn_fee
        ).submit()
        
        listing_box.delete()
        
        return String("Listing Fulfilled for Asset: "), asset_listed.id, String("Lister received x amount of Algo: "), payment.amount       
        
    @abimethod
    def cancelListingRequest(
        self,
        listing_asset: Asset,
        listing_name: listingName,
        transfer_fee: gtxn.PaymentTransaction
    ) -> String:
        
        assert transfer_fee.amount == 1000
        assert listing_name.lister.native == Txn.sender
        
        listing_box = BoxRef(key=listing_name.bytes)
        
        value, exists = listing_box.maybe()
        
        assert exists
        
        listing_value = listingValue.from_bytes(value)
        
        assert listing_value.asset_listed == listing_asset.id
        
        itxn.AssetTransfer(
            xfer_asset=listing_asset.id,
            asset_amount=1,
            asset_receiver=Txn.sender,
            fee=Global.min_txn_fee
        ).submit()
        
        listing_box.delete()
        
        return String("Listing Request Cancelled")

Ensure you have the necessary imports and class definitions for your contract.

Step 5: Create Two Arbitrary Assets for Experimenting

Follow the steps to create and configure two assets for testing purposes.

from algosdk.v2client.algod import AlgodClient
from algosdk.transaction import AssetConfigTxn, wait_for_confirmation
from algosdk.account import address_from_private_key
import os
from dotenv import load_dotenv

load_dotenv()

algod_token = os.getenv('algod_token')
algod_server = os.getenv('algod_server')
algod_client = AlgodClient(algod_token, algod_server)

private_key = os.getenv('buyer_private_key')
address = address_from_private_key(private_key)

params = algod_client.suggested_params()

asset_creation_txn = AssetConfigTxn(
    sender=address,
    sp=params,
    total=100,
    default_frozen=False,
    asset_name='Test Asset Two',
    unit_name='TA2',
    manager=address,
    reserve=address,
    strict_empty_address_check=False,
)

signed_tx = asset_creation_txn.sign(private_key)
tx_id = algod_client.send_transaction(signed_tx)
print(tx_id)

wait_for_confirmation(algod_client, tx_id)

asset_id = algod_client.pending_transaction_info(tx_id)['asset-index']
asset_info = algod_client.asset_info(asset_id)
print(asset_info)

Step 6: Store Generated Asset IDs in .env

asset_1 = 123456
asset_2 = 654321

Step 7: Experiment with Opting the Contract into an Asset

If you're creating multiple listings, you need to opt the contract into each asset the first time.

from algokit_utils import ApplicationClient
from algosdk.v2client.algod import AlgodClient
from algosdk.account import address_from_private_key
from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionWithSigner, AtomicTransactionComposer
from algosdk.transaction import PaymentTxn
from pathlib import Path
import os
from dotenv import load_dotenv

load_dotenv()

algod_token = os.getenv('algod_token')
algod_server = os.getenv('algod_server')
algod_client = AlgodClient(algod_token, algod_server)

private_key = os.getenv('private_key')
address = address_from_private_key(private_key)

app_spec = Path(__file__).parent / './listings.arc32.json'
app_id = int(os.getenv('app_id'))
signer = AccountTransactionSigner(private_key)
params = algod_client.suggested_params()

app_client = ApplicationClient(
    algod_client=algod_client,
    app_spec=app_spec,
    app_id=app_id,
    signer=signer,
    sender=address,
    suggested_params=params,
)

atc = AtomicTransactionComposer()

asset_1 = int(os.getenv('asset_1'))

fee_payment_tx = PaymentTxn(sender=address, sp=params, receiver=app_client.app_address, amt=101_000)
wrapped_payment = TransactionWithSigner(fee_payment_tx, signer)

app_client.compose_call(atc, call_abi_method='triggerOptIn', asset_to_opt_into=asset_1, fee_payment=wrapped_payment)

results = atc.execute(algod_client, 2)

tx_ids = [results.tx_ids[i] for i in range(len(results.tx_ids))]
abi_results = [results.abi_results[i].return_value for i in range(len(results.abi_results))]

print(tx_ids)
print(abi_results)

Step 8: Make an Asset for Algo Listing Box (All listing types are supported from parts 1-4)

Create an asset listing box to manage your asset listings.

from algokit_utils import ApplicationClient
from algosdk.v2client.algod import AlgodClient
from algosdk.account import address_from_private_key
from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionWithSigner, AtomicTransactionComposer
from algosdk.transaction import PaymentTxn, AssetTransferTxn
from pathlib import Path
from algosdk.util import algos_to_microalgos
from algosdk.abi import ABIType
from base64 import b64decode
import os
from dotenv import load_dotenv

load_dotenv()

algod_token = os.getenv('algod_token')
algod_server = os.getenv('algod_server')
algod_client = AlgodClient(algod_token, algod_server)

private_key = os.getenv('private_key')
address = address_from_private_key(private_key)

app_spec = Path(__file__).parent / './listings.arc32.json'
app_id = int(os.getenv('app_id'))
signer = AccountTransactionSigner(private_key)
params = algod_client.suggested_params()

app_client = ApplicationClient(
    algod_client=algod_client,
    app_spec=app_spec,
    app_id=app_id,
    signer=signer,
    sender=address,
    suggested_params=params,
)

atc = AtomicTransactionComposer()

asset_1 = int(os.getenv('asset_1'))

fee_payment_tx = PaymentTxn(sender=address, sp=params, receiver=app_client.app_address, amt=101_000)
wrapped_payment = TransactionWithSigner(fee_payment_tx, signer)

app_client.compose_call(
    atc, 
    call_abi_method='triggerOptIn', 
    asset_to_opt_into=asset_1, 
    fee_payment=wrapped_payment)


asset_to_list_transaction = AssetTransferTxn(sender=address, sp=params, receiver=app_client.app_address, amt=1, index=asset_1)
wrapped_asset_list_transaction = TransactionWithSigner(asset_to_list_transaction, signer)

box_fee_payment_tx = PaymentTxn(sender=address, sp=params, receiver=app_client.app_address, amt=24_900)
wrapped_box_fee_payment = TransactionWithSigner(box_fee_payment_tx, signer)

current_global_listing_counter = algod_client.application_info(app_id)['params']['global-state'][0]['value']['bytes']
uint64_coder = ABIType.from_string('(uint64)')
b64_decoded_global_listing_counter = b64decode(current_global_listing_counter)
encoded_global_listing_counter = uint64_coder.decode(b64_decoded_global_listing_counter)[0]

listing_box_counter = encoded_global_listing_counter + 1
listing_box_coder = ABIType.from_string('(address,uint64)')
users_listing_box_name = listing_box_coder.encode((address, listing_box_counter))

app_client.compose_call(
    atc, 
    call_abi_method='postListingRequest', 
    asset_to_list=wrapped_asset_list_transaction, 
    algo_amount_requested=algos_to_microalgos(1), 
    box_fee_mbr_payment=wrapped_box_fee_payment,
    transaction_parameters={'boxes': [[app_id, users_listing_box_name]]}
)

results = atc.execute(algod_client, 2)

tx_ids = [results.tx_ids[i] for i in range(len(results.tx_ids))]
abi_results = [results.abi_results[i].return_value for i in range(len(results.abi_results))]

print(tx_ids)
print(abi_results)

Step 9: Check Respective Listing Information

Retrieve and verify the listing information from the contract.

import os
from algosdk.v2client.algod import AlgodClient
from algosdk.abi import ABIType
from base64 import b64decode
from dotenv import load_dotenv

load_dotenv()

algod_token = os.getenv('algod_token')
algod_server = os.getenv('algod_server')
algod_client = AlgodClient(algod_token, algod_server)

app_id = int(os.getenv('app_id'))

boxes = algod_client.application_boxes(app_id)['boxes']

for box in boxes:
    box_name_b64encoded = box['name']
    print(b64decode(box_name_b64encoded))
    box_value = b64decode(algod_client.application_box_by_name(app_id, b64decode(box_name_b64encoded))['value'])
    print(box_value)

    #Asset for Algo: '(uint64,uint64)'
    #Asset for Asset: '(uint64,uint64,bool)'
    #Asset for Asset Quantity: '(uint64,uint64,uint64,bool)
    #Asset Quantity for Asset Quantity: '(uint64,uint64,uint64,uint64,bool)'
    #Universal Listing: '(uint64,uint64,uint64,uint64,bool)'
    listing_box_value_coder = ABIType.from_string('(uint64,uint64)')
    print(listing_box_value_coder.decode(box_value))

Step 10: Purchase the Asset with Your Buyer Private Key

When generating a listing, the counter variable should be incremented.

counter = 1

Note; counter should match the number of listings generated so far.

Execute the purchase of the asset using the buyer's private key.

from algokit_utils import ApplicationClient
from algosdk.v2client.algod import AlgodClient
from algosdk.account import address_from_private_key
from algosdk.atomic_transaction_composer import AccountTransactionSigner, TransactionWithSigner, AtomicTransactionComposer
from algosdk.transaction import PaymentTxn, AssetTransferTxn
from pathlib import Path
from algosdk.abi import ABIType
import os
from dotenv import load_dotenv

load_dotenv()

algod_token = os.getenv('algod_token')
algod_server = os.getenv('algod_server')
algod_client = AlgodClient(algod_token, algod_server)

private_key = os.getenv('buyer_private_key')
address = address_from_private_key(private_key)

app_spec = Path(__file__).parent / './listings.arc32.json'
app_id = int(os.getenv('app_id'))
signer = AccountTransactionSigner(private_key)
params = algod_client.suggested_params()

app_client = ApplicationClient(
    algod_client=algod_client,
    app_spec=app_spec,
    app_id=app_id,
    signer=signer,
    sender=address,
    suggested_params=params,
)

atc = AtomicTransactionComposer()

asset_1 = int(os.getenv('asset_1'))

asset_opt_in_transaction = AssetTransferTxn(sender=address, sp=params, receiver=address, amt=0, index=asset_1)
wrapped_opt_in_transaction = TransactionWithSigner(asset_opt_in_transaction, signer)
atc.add_transaction(wrapped_opt_in_transaction)

asset_payment_tx = PaymentTxn(sender=address, sp=params, receiver=app_client.app_address, amt=1_000_000)
wrapped_asset_payment = TransactionWithSigner(asset_payment_tx, signer)

fee_payment_tx = PaymentTxn(sender=address, sp=params, receiver=app_client.app_address, amt=2000)
wrapped_fee_payment = TransactionWithSigner(fee_payment_tx, signer)

lister_address = address_from_private_key(os.getenv('private_key'))
counter = 1 #Remember to increment counter for additional listings
listing_box_coder = ABIType.from_string('(address,uint64)')
box_name = listing_box_coder.encode((lister_address, counter))

app_client.compose_call(
    atc, 
    call_abi_method='fulfillListingRequest', 
    asset_listed=asset_1, 
    listing_name=(lister_address,counter), 
    payment=wrapped_asset_payment, 
    transfer_fees=wrapped_fee_payment, 
    transaction_parameters={'boxes': [[app_id, box_name]], 'accounts':[lister_address]})

results = atc.execute(algod_client, 2)

tx_ids = [results.tx_ids[i] for i in range(len(results.tx_ids))]
abi_results = [results.abi_results[i].return_value for i in range(len(results.abi_results))]

print(tx_ids)
print(abi_results)

Code Editor