ARC69 Minter and Modifier Part 1

Video Tutorial

Contract - Registering and Determining the User's Pokemon

Compile the contract using:

algokit compile arc69NFTmodifier.py

Python Code

from algopy import ARC4Contract, Global, GlobalState, gtxn, BoxRef, Txn, op
from algopy.arc4 import abimethod, Struct
from algopy.arc4 import UInt64 as arc4UInt64
from algopy.arc4 import String as arc4String

class availablePokemon(Struct):
    pokemon_name: arc4String
    pokemon_type: arc4String
    pokemon_description: arc4String
    pokemon_ipfs_hash: arc4String

class arc69NFTmodifier(ARC4Contract):
    def __init__(self) -> None:
        self.pokemonUnitCounter = GlobalState(arc4UInt64(0))
    
    @abimethod
    def registerNewPokemonData(
        self,
        pokemon_name: arc4String,
        pokemon_type: arc4String,
        pokemon_description: arc4String,
        pokemon_ipfs_hash: arc4String,
        payment_txn: gtxn.PaymentTransaction,
    ) -> arc4String:
        
        assert payment_txn.receiver == Global.current_application_address
        self.pokemonUnitCounter.value = arc4UInt64(self.pokemonUnitCounter.value.native + 1)
        
        box_ref = BoxRef(key=self.pokemonUnitCounter.value.bytes)
        value, exists = box_ref.maybe()
        assert not exists
        
        new_pokemon_info = availablePokemon(
            pokemon_name=pokemon_name,
            pokemon_type=pokemon_type,
            pokemon_description=pokemon_description,
            pokemon_ipfs_hash=pokemon_ipfs_hash
        )
        
        box_ref.create(size=new_pokemon_info.bytes.length)
        box_ref.put(new_pokemon_info.bytes)
        
        return arc4String('Pokemon Registered to Contract: ') + pokemon_name

    @abimethod
    def determineUserPokemon(
        self,
        payment_txn: gtxn.PaymentTransaction
    ) -> tuple[arc4String, arc4UInt64]:
        
        assert payment_txn.amount == 18_500
        assert payment_txn.receiver == Global.current_application_address
        
        randomizer = arc4UInt64(
            Global.latest_timestamp + Global.round + arc4UInt64.from_bytes(Txn.tx_id[0:8]).native % arc4UInt64.from_bytes(Txn.tx_id[-8:]).native
        )
        
        hashed_randomizer = op.sha256(randomizer.bytes)
        
        pokemon_selection = arc4UInt64((arc4UInt64.from_bytes(hashed_randomizer[0:8]).native % self.pokemonUnitCounter.value.native) + 1)
        
        users_claim_box = BoxRef(key=Txn.sender.bytes)
        value, exists = users_claim_box.maybe()
        assert not exists
        
        users_claim_box.create(size=8)
        users_claim_box.put(pokemon_selection.bytes)
        
        return arc4String('Users Pokemon # Selected:'), pokemon_selection
        

Setting Up Environment Variables (.env)

private_key = ENTER PRIVATE KEY
address = ENTER ADDRESS (OPTIONAL)
api_key = ENTER API KEY
api_secret = ENTER API SECRET
jwt = ENTER JWT
algod_token = 
algod_server = https://testnet-api.4160.nodely.dev
app_id = ENTER APP ID GENERATED

Upload Pokemon Images to IPFS

Charmander Image Bulbasaur Image Squirtle Image
from pinata import Pinata
from dotenv import load_dotenv
import os

load_dotenv()

api_key = os.getenv('api_key')
api_secret = os.getenv('api_secret')
jwt = os.getenv('jwt')

pinata = Pinata(api_key=api_key, secret_key=api_secret, access_token=jwt)

file_names = ['./pokeimages/bulba.png','./pokeimages/charmander.png','./pokeimages/squirtle.png']
for name in file_names:
    response = pinata.pin_file(name)
    image_ipfs_hash = response['data']['IpfsHash']
    viewable_hash = f'https://gateway.pinata.cloud/ipfs/' + image_ipfs_hash
    print(image_ipfs_hash, viewable_hash)

Registering Pokemon for Minting to the Contract

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

load_dotenv()

node_token = os.getenv('algod_token')
node_server = os.getenv('algod_server')

private_key = os.getenv('private_key')

algod_client = AlgodClient(node_token, node_server)
app_spec = Path(__file__).parent / './arc69NFTmodifier.arc32.json'
app_id = int(os.getenv('app_id'))
signer = AccountTransactionSigner(private_key=private_key)
address = address_from_private_key(private_key=private_key)
params = algod_client.suggested_params()

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

pokemon_name_1 = 'Bulbasaur'
pokemon_name_2 = 'Charmander'
pokemon_name_3 = 'Squirtle'

pokemon_type_1 = 'Grass'
pokemon_type_2 = 'Fire'
pokemon_type_3 = 'Water'

pokemon_description_1 = 'A grass pokemon'
pokemon_description_2 = 'A fire pokemon'
pokemon_description_3 = 'A water pokemon'

pokemon_ipfs_hash_1 = 'Qmc75zNUFoFFX3uPcjnUMqhvuzezHF9tTpgbf5pph2XegF'
pokemon_ipfs_hash_2 = 'QmP2M7nPDjhU9hPr8ManjzyFzBVEJDT3LKAAKC2o5f639q'
pokemon_ipfs_hash_3 = 'QmRbPCwbQ7vps64kPUHygCkSmW36FkCLbET9X7cEjtPHJV'

atc = AtomicTransactionComposer()

pokemon_info_coder = ABIType.from_string('(string,string,string,string,uint64)')
total_bytes_1 = len(pokemon_info_coder.encode((pokemon_name_1, pokemon_type_1, pokemon_description_1, pokemon_ipfs_hash_1, 0)))
total_bytes_2 = len(pokemon_info_coder.encode((pokemon_name_2, pokemon_type_2, pokemon_description_2, pokemon_ipfs_hash_2, 0)))
total_bytes_3 = len(pokemon_info_coder.encode((pokemon_name_3, pokemon_type_3, pokemon_description_3, pokemon_ipfs_hash_3, 0)))

box_cost_1 = 2500 + (400 * total_bytes_1)
box_cost_2 = 2500 + (400 * total_bytes_2)
box_cost_3 = 2500 + (400 * total_bytes_3)

mbr_fee_payment_tx_1 = PaymentTxn(
    sender=address,
    sp=params,
    receiver=application_client.app_address,
    amt=box_cost_1,
    note='#1'
)
wrapped_payment_tx_1 = TransactionWithSigner(mbr_fee_payment_tx_1, signer)

mbr_fee_payment_tx_2 = PaymentTxn(
    sender=address,
    sp=params,
    receiver=application_client.app_address,
    amt=box_cost_2,
    note='#2'
)
wrapped_payment_tx_2 = TransactionWithSigner(mbr_fee_payment_tx_2, signer)

mbr_fee_payment_tx_3 = PaymentTxn(
    sender=address,
    sp=params,
    receiver=application_client.app_address,
    amt=box_cost_3,
    note='#3'
)
wrapped_payment_tx_3 = TransactionWithSigner(mbr_fee_payment_tx_3, signer)

box_ref_1 = (1).to_bytes(8, 'big')
box_ref_2 = (2).to_bytes(8, 'big')
box_ref_3 = (3).to_bytes(8, 'big')

application_client.compose_call(
    atc, 
    call_abi_method='registerNewPokemonData', 
    pokemon_name=pokemon_name_1, 
    pokemon_type=pokemon_type_1,
    pokemon_description=pokemon_description_1,
    pokemon_ipfs_hash=pokemon_ipfs_hash_1,
    payment_txn=wrapped_payment_tx_1,
    transaction_parameters={
        'boxes':[[app_id, box_ref_1]]
    }
)

application_client.compose_call(
    atc, 
    call_abi_method='registerNewPokemonData', 
    pokemon_name=pokemon_name_2, 
    pokemon_type=pokemon_type_2,
    pokemon_description=pokemon_description_2,
    pokemon_ipfs_hash=pokemon_ipfs_hash_2,
    payment_txn=wrapped_payment_tx_2,
    transaction_parameters={
        'boxes':[[app_id, box_ref_2]]
    }
)

application_client.compose_call(
    atc, 
    call_abi_method='registerNewPokemonData', 
    pokemon_name=pokemon_name_3, 
    pokemon_type=pokemon_type_3,
    pokemon_description=pokemon_description_3,
    pokemon_ipfs_hash=pokemon_ipfs_hash_3,
    payment_txn=wrapped_payment_tx_3,
    transaction_parameters={
        'boxes':[[app_id, box_ref_3]]
    }
)

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)

Determining the User's Pokemon

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

load_dotenv()

node_token = os.getenv('algod_token')
node_server = os.getenv('algod_server')

private_key = os.getenv('private_key')

algod_client = AlgodClient(node_token, node_server)
app_spec = Path(__file__).parent / './arc69NFTmodifier.arc32.json'
app_id = int(os.getenv('app_id'))
signer = AccountTransactionSigner(private_key=private_key)
address = address_from_private_key(private_key=private_key)
params = algod_client.suggested_params()

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


atc = AtomicTransactionComposer()

mbr_fee_payment_tx_1 = PaymentTxn(
    sender=address,
    sp=params,
    receiver=application_client.app_address,
    amt=18_500,
    note='#1'
)
wrapped_payment_tx_1 = TransactionWithSigner(mbr_fee_payment_tx_1, signer)


box_ref_1 = decode_address(address)


application_client.compose_call(
    atc, 
    call_abi_method='determineUserPokemon', 
    payment_txn=wrapped_payment_tx_1,
    transaction_parameters={
        'boxes':[[app_id, box_ref_1]]
    }
)


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)

Confirming Existence of Pokemon Info in Contract

from algosdk.v2client.algod import AlgodClient
import os
from base64 import b64decode
from algosdk.abi import ABIType
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'))
app_boxes = algod_client.application_boxes(app_id)['boxes']

pokemon_info_coder = ABIType.from_string('(string,string,string,string)')

for box in app_boxes:
    box_name = b64decode(box['name'])
    if len(box_name) == 8: #This is a pokemon info box 
        box_value = algod_client.application_box_by_name(app_id, box_name)['value']
        decoded_box_value = pokemon_info_coder.decode(b64decode(box_value))
        print(decoded_box_value)
        

Confirming Pokemon Selected for the User

from algosdk.v2client.algod import AlgodClient
import os
from base64 import b64decode
from algosdk.abi import ABIType
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'))
app_boxes = algod_client.application_boxes(app_id)['boxes']

pokemon_info_coder = ABIType.from_string('(uint64)')

for box in app_boxes:
    box_name = b64decode(box['name'])
    if len(box_name) == 32: #This is the users selected pokemon ready to claim
                            #contains only the pokemon #, eg; pokemon #1, pokemon #2, or pokemon #3 
        box_value = algod_client.application_box_by_name(app_id, box_name)['value']
        decoded_box_value = pokemon_info_coder.decode(b64decode(box_value))
        print(decoded_box_value)
        

Code Editor