ARC69 Minter and Modifier Part 3
Video Tutorial
Contract - User Sends Pokemon To Training (Stakes) and Removes from Training (Unstakes)
Compile the contract using:
algokit compile arc69NFTmodifier.py
from algopy import ARC4Contract, itxn, Global, GlobalState, UInt64, gtxn, Bytes, subroutine, String, BoxRef, Txn, op, LocalState, Account
from algopy.arc4 import abimethod, Struct, Address, Bool
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 userPokemonInfo(Struct):
uid: arc4UInt64
owner: Address
asset_id: arc4UInt64
pokemon_id: arc4UInt64
level: arc4UInt64
exp: arc4UInt64
training: Bool
training_start_time: arc4UInt64
class arc69NFTmodifier(ARC4Contract):
def __init__(self) -> None:
self.pokemonUnitCounter = GlobalState(arc4UInt64(0))
self.pokemonCreated = GlobalState(arc4UInt64(0))
@subroutine
def itoa(self, i: UInt64) -> Bytes:
digits = Bytes(b"0123456789")
radix = digits.length
if i < radix:
return digits[i]
return self.itoa(i // radix) + digits[i % radix]
@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.amount
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
@abimethod()
def mintUserPokemon(
self,
payment_txn: gtxn.PaymentTransaction
) -> tuple[arc4String, UInt64]:
assert payment_txn.amount >= 152900
assert payment_txn.receiver == Global.current_application_address
users_claim_box = BoxRef(key=Txn.sender.bytes)
claim_box_value, exists = users_claim_box.maybe()
assert exists
pokemon_selection = arc4UInt64.from_bytes(claim_box_value)
pokemon_info_bytes = BoxRef(key=pokemon_selection.bytes)
pokemon_info_value, exists = pokemon_info_bytes.maybe()
assert exists
available_pokemon_info = availablePokemon.from_bytes(pokemon_info_value)
unit_name_int_as_string = String.from_bytes(self.itoa(pokemon_selection.native))
uid_as_string = String.from_bytes(self.itoa(self.pokemonCreated.value.native))
pokemon_creation_tx = itxn.AssetConfig(
total=1,
manager=Global.current_application_address,
asset_name=available_pokemon_info.pokemon_name.native,
unit_name='PK#' + unit_name_int_as_string,
decimals=0,
url= 'https://gateway.pinata.cloud/ipfs/' + available_pokemon_info.pokemon_ipfs_hash.native,
note = (
'{"standard": "arc69", "mime_type": "image/png", "properties": {'
'"Type": "' + available_pokemon_info.pokemon_type.native + '", '
'"Description": "' + available_pokemon_info.pokemon_description.native + '", '
'"Level": "1", '
'"Experience": "0", '
'"Pokemon Number": "' + unit_name_int_as_string + '", '
'"UID": "' + uid_as_string + '" }}'
),
fee=Global.min_txn_fee
).submit()
user_pokemon_box = BoxRef(key=self.pokemonCreated.value.bytes + Txn.sender.bytes)
user_pokemon_box_value, exists = user_pokemon_box.maybe()
assert not exists
new_user_pokemon_info = userPokemonInfo(
uid=self.pokemonCreated.value,
owner=Address(Txn.sender),
asset_id=arc4UInt64(pokemon_creation_tx.created_asset.id),
pokemon_id=pokemon_selection,
level=arc4UInt64(1),
exp=arc4UInt64(0),
training=Bool(False),
training_start_time=arc4UInt64(0),
)
user_pokemon_box.create(size=new_user_pokemon_info.bytes.length)
user_pokemon_box.put(new_user_pokemon_info.bytes)
users_claim_box.delete()
self.pokemonCreated.value = arc4UInt64(self.pokemonCreated.value.native + 1)
return arc4String('User pokemon generated, asset ID: '), pokemon_creation_tx.created_asset.id
@abimethod
def claimPokemon(
self,
opt_in_txn: gtxn.AssetTransferTransaction,
uid: arc4UInt64
) -> String:
user_pokemon_box = BoxRef(key=uid.bytes + Txn.sender.bytes)
user_pokemon_box_value, exists = user_pokemon_box.maybe()
assert exists
users_pokemon_info = userPokemonInfo.from_bytes(user_pokemon_box_value)
#assert users_pokemon_info.owner == Address(Txn.sender)
#assert opt_in_txn.xfer_asset.id == users_pokemon_info.asset_id
itxn.AssetTransfer(
asset_receiver=Txn.sender,
xfer_asset=users_pokemon_info.asset_id.native,
fee=Global.min_txn_fee,
asset_amount=1
).submit()
return String('User received their pokemon')
@abimethod
def trainPokemon(
self,
stake_pokemon_tx: gtxn.AssetTransferTransaction,
uid: arc4UInt64
) -> String:
assert stake_pokemon_tx.asset_amount == 1
assert stake_pokemon_tx.asset_receiver == Global.current_application_address
pokemon_to_train = stake_pokemon_tx.xfer_asset.id
users_pokemon_info = BoxRef(key=uid.bytes + Txn.sender.bytes)
value, exists = users_pokemon_info.maybe()
assert exists
current_pokemon_info = userPokemonInfo.from_bytes(value)
assert pokemon_to_train == current_pokemon_info.asset_id
current_pokemon_info.training_start_time = arc4UInt64(Global.latest_timestamp)
current_pokemon_info.training = Bool(True)
users_pokemon_info.put(current_pokemon_info.bytes)
return String("Pokemon Staked")
@abimethod
def removePokemonFromTraining(
self,
pokemon_requested_uid: arc4UInt64
) -> String:
users_pokemon_info_box = BoxRef(key=pokemon_requested_uid.bytes + Txn.sender.bytes)
users_pokemon_info_value, exists = users_pokemon_info_box.maybe()
assert exists
current_users_pokemon_info = userPokemonInfo.from_bytes(users_pokemon_info_value)
general_pokemon_info_box = BoxRef(key=current_users_pokemon_info.pokemon_id.bytes)
general_pokemon_info_value, exists = general_pokemon_info_box.maybe()
general_pokemon_info = availablePokemon.from_bytes(general_pokemon_info_value)
unit_name_as_string = String.from_bytes(self.itoa(current_users_pokemon_info.pokemon_id.native))
initial_training_time = current_users_pokemon_info.training_start_time.native
current_time = Global.latest_timestamp
time_difference = current_time - initial_training_time
current_level = current_users_pokemon_info.level.native
current_experience = current_users_pokemon_info.exp.native
base_experience = UInt64(200)
cumulative_experience = current_level * base_experience
total_experience = current_experience + time_difference
if total_experience >= cumulative_experience:
levels_gained = (total_experience + base_experience - cumulative_experience) // base_experience
new_level = current_level + levels_gained
new_experience = total_experience - ((new_level - 1) * base_experience)
else:
new_level = current_level
new_experience = total_experience
new_level_string = String.from_bytes(self.itoa(new_level))
new_experience_string = String.from_bytes(self.itoa(new_experience))
uid_as_string = String.from_bytes(self.itoa(current_users_pokemon_info.uid.native))
itxn.AssetConfig(
sender=Global.current_application_address,
manager=Global.current_application_address,
config_asset=current_users_pokemon_info.asset_id.native,
note = (
'{"standard": "arc69", "mime_type": "image/png", "properties": {'
'"Type": "' + general_pokemon_info.pokemon_type.native + '", '
'"Description": "' + general_pokemon_info.pokemon_description.native + '", '
'"Level": "' + new_level_string + '", '
'"Experience": "' + new_experience_string + '", '
'"Pokemon Number": "' + unit_name_as_string + '", '
'"UID": "' + uid_as_string + '" }}'
),
fee=Global.min_txn_fee
).submit()
itxn.AssetTransfer(
asset_receiver=Txn.sender,
asset_amount=1,
xfer_asset=current_users_pokemon_info.asset_id.native,
fee=Global.min_txn_fee
).submit()
new_users_pokemon_info = current_users_pokemon_info.copy()
new_users_pokemon_info.level = arc4UInt64(new_level)
new_users_pokemon_info.exp = arc4UInt64(new_experience)
new_users_pokemon_info.training = Bool(False)
users_pokemon_info_box.put(new_users_pokemon_info.bytes)
return String('Pokemon Removed from Stake')
Seeing the Current Status of the User's Pokemon
Note: The UID in the metadata and the user's address is the box name
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,address,uint64,uint64,uint64,uint64,bool,uint64)')
for box in app_boxes:
box_name = b64decode(box['name'])
if len(box_name) == 40: #This is user specific pokemon information (box name is uid for pokemon [8 bytes] + user address [32 bytes])
box_value = algod_client.application_box_by_name(app_id, )['value']
decoded_box_value = pokemon_info_coder.decode(b64decode(box_value))
print(decoded_box_value)
Executing in Python
The Pokemon Level-up System
Note: Experience and level gain are calculated based on time staked, with each second providing an additional experience point.
initial_training_time = current_users_pokemon_info.training_start_time.native
current_time = Global.latest_timestamp
time_difference = current_time - initial_training_time
current_level = current_users_pokemon_info.level.native
current_experience = current_users_pokemon_info.exp.native
base_experience = UInt64(200)
cumulative_experience = current_level * base_experience
total_experience = current_experience + time_difference
if total_experience >= cumulative_experience:
levels_gained = (total_experience + base_experience - cumulative_experience) // base_experience
new_level = current_level + levels_gained
new_experience = total_experience - ((new_level - 1) * base_experience)
else:
new_level = current_level
new_experience = total_experience
Staking the 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, AssetTransferTxn
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
from base64 import b64decode
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()
decoded_address = decode_address(address)
users_pokemon = 728462064
stake_pokemon_tx = AssetTransferTxn(
sender=address,
receiver=application_client.app_address,
sp=params,
amt=1,
index=users_pokemon
)
wrapped_stake_tx = TransactionWithSigner(stake_pokemon_tx, signer)
current_uid = (0).to_bytes(8, 'big')
box_ref_1 = current_uid + decoded_address
application_client.compose_call(
atc,
call_abi_method='trainPokemon',
stake_pokemon_tx=wrapped_stake_tx,
uid=0,
transaction_parameters={
'boxes':[[app_id, box_ref_1]],
'foreign_assets': [users_pokemon]
}
)
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)
Unstaking the 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, AssetTransferTxn
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
from base64 import b64decode
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()
decoded_address = decode_address(address)
mbr_fee_payment_tx_1 = PaymentTxn(
sender=address,
sp=params,
receiver=application_client.app_address,
amt=3000,
note='#1'
)
wrapped_payment_tx_1 = TransactionWithSigner(mbr_fee_payment_tx_1, signer)
atc.add_transaction(wrapped_payment_tx_1)
users_pokemon = 728462064
stake_pokemon_tx = AssetTransferTxn(
sender=address,
receiver=application_client.app_address,
sp=params,
amt=1,
index=users_pokemon
)
wrapped_stake_tx = TransactionWithSigner(stake_pokemon_tx, signer)
current_uid = (0).to_bytes(8, 'big')
box_ref_1 = current_uid + decoded_address
box_ref_2 = (3).to_bytes(8, 'big')
application_client.compose_call(
atc,
call_abi_method='removePokemonFromTraining',
pokemon_requested_uid=0,
transaction_parameters={
'boxes':[[app_id, box_ref_1], [app_id, box_ref_2]],
'foreign_assets': [users_pokemon]
}
)
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)