#How do I convert between algopy and arc4 types (e.g., UInt64)?

1 messages · Page 1 of 1 (latest)

gritty pewter
#

I'm trying to use a BoxMap of Structs to store state, and I get an error if the arc4.Struct is defined without arc4 types, but when I try to assign a regular UInt64 to an arc4.UInt64, it also fails.

main totem
#

Can you share the code you've got that's not working and the error message?

gritty pewter
#

It appears that just wrapping a ctor around them (e.g., calling arc4.UInt64(my_int_val)) works for the integers.

#

But I have this: ```class Stake(arc4.Struct, kw_only=True):
'''
owner_address (arc4.Address): the address of the staker
stake_amount (UInt64): the number of microalgos staked
stake_period_in_days (UInt64): the number of days to stake the amount
stake_apy (UInt64): the apy in basis points (e.g., 1/100ths of a %age point)
'''
owner_address : arc4.Address
stake_amount : arc4.UInt64
stake_period_in_days : arc4.UInt64
stake_apy : arc4.UInt64
nft_asset_id : arc4.UInt64
initial_block_no : arc4.UInt64
last_paid_at_ts : arc4.UInt64
accrued_rewards : arc4.UInt64
is_funded : bool

#

seems like an implicity type conversion here would be useful syntactic sugar, though I recognize it would add potential ambiguity into the teal synthesis.

#

Also seems like I can't get an algopy.UInt64 back by wrapping the arc4.UInt64 in the ctor

main totem
gritty pewter
#

So the conversion to the arc4 type is easy.. converting back from it seems less sobvious?

#

or do I need to do an op.btoi on it?

main totem
gritty pewter
#

Some of them are being used for arithmetic. Some of them are just identifiers. I'm using the asset ID for an nft as a key for a BoxMap -- so that one is fine just as an arc4.UInt64. The ones that I'm uisng as accumulators or otherwise inputs to arithmetic are more problematic, because I need to be able to do bidirectional conversions with them.

#

(the link you sent includes nothing about turning the arc4.UInt64 into a regular algopy.UInt64)

#

oh, wait!

#

I can call arc4.UInt64.native to get the algopy.UInt64 back?

#

Ok. so for the ints that makes sense. For account/address -- is arc4.Address the arc4 equivalent of algopy.Account?

#

Or should I be using something else?

main totem
main totem
gritty pewter
main totem
#

Address is different--that's not a UInt64

gritty pewter
#

I know.

main totem
#

But for asset IDs, e.g.

gritty pewter
#

Ah, for asset IDs use asset rather than UInt64?

main totem
#

Yeah

#

nft_asset_id : arc4.UInt64

gritty pewter
#

but there is no arc4.Asset type?

main totem
#

You can use algopy.Asset

gritty pewter
#

and that will be legal in an arc4.struct?

#

itxn.AssetConfig returns a UInt64, not an algopy.Asset

main totem
#

On a return that's fine

#

Part of the reason that it can be helpful to define a method arg as Asset instead of just UInt64 is because that asset will need to be provided as a foreign asset reference on the app call

#

So it is a "special" UInt64, not just any UInt64

gritty pewter
#

Ok, so I define it as an Asset in method signature, but it's still a UInt64 for purposes of keying the BoxMap, and for purposes of return value?

#

That still leaves me with Account...I'mm not allowed to use algopy.Account in the struct, but it rejects the attempt to convert it to arc4.Address. What's the best way to manage that?

#

Just treat it as "bytes"?

gritty pewter
#

So you saw the struct definition, with owner_address set to arc4.Address -- I'd previously had it set to algopy.Account -- because I set it to Txn.sender

#

I tried setting arc4.Address to Txn.sender.bytes, but that also fails.

main totem
#

Have you referred to this BoxMap example from the docs?

from algopy import BoxMap, Contract, Account, Txn, String

class MyContract(Contract):
    def __init__(self) -> None:
        self.my_map = BoxMap(Account, String, key_prefix=b"a_")
    
    def approval_program(self) -> bool:        
        # Check if the box exists
        if Txn.sender in self.my_map:
            # Reassign the value
            self.my_map[Txn.sender] = String(" World")
        else:
            # Assign a new value
            self.my_map[Txn.sender] = String("Hello")
        # Read a value
        return self.my_map[Txn.sender] == String("Hello World")```
#

Account should work here

gritty pewter
#

Yeah, but I'm not using the Account as the BoxMap key -- I'm using the Asset. The Acccount I'm storing as an attribute on the arc4.struct

#

I was using the account as the boxmap key, and then I realized that I wanted the same wallet to be able to have multiple stakes, so I changed it to look up by NFT asset ID, and then confirm that Stake.owner_address matched the wallet interacting with it.

main totem
#

why not key to a struct of [Account, Asset] ?

gritty pewter
#

That's a good idea, though it still doesn't solve the question of how to store an Account inside of a struct

#

Is converting the Account to an arc4.Address the right approach?

main totem
gritty pewter
#

Right, but I want to be able to look it up? Or is that fundamentally unnecessary, because I can query the chain for the asset owner?

main totem
#

You can assemble the Account+Asset bytes offchain and query the box by that name

#

Just as an example, if a bit extreme, I have an app that takes about a dozen inputs, hashes them, and then uses the resulting bytes as the box key. I then have an effectively-infinite number of box keys I can use, and I can do the hash off-chain anytime I need to fetch a box value from algod

gritty pewter
#

I guess if it's a composite key, it wouldn't need to be a property on the struct -- the user would pass in the asset id, we'd concatenate it with Txn.sender to create the composite key, and look up the details, so they would have no need to be stored there. So for this specific use case, I can avoid it altogether.

#

But...for future use...if I ever had a reason to store an Account in a box, what is the correct arc4 data structure to use?

#

One possible argument (easily worked around with off-chain caching of the tuple, but for argument's sake) would be:
let's say I had an off-chain process that iterated over NFTs and paid a dividend to the account of each in sequence...I'd like the recipient account to be easily accessible on-chain so we could just pass in the nft id, after iterating over that list in isolation?

gritty pewter
#

That shows using the Account as the key

#

But to store an Account in a struct, it's required to be an arc4 type

#

Invalid ARC4 Struct declaration, the following fields are not ARC4 encoded types: owner_address

#

that's the build error I get when I have an algopy.Account as a data member of an arc4.struct

main totem
#

show me the struct in total so I can try to recreate it?

gritty pewter
#
    owner_address : Account
    stake_amount : arc4.UInt64
    stake_period_in_days : arc4.UInt64
    stake_apy : arc4.UInt64
    nft_asset_id : UInt64
    initial_block_no : arc4.UInt64
    last_paid_at_ts : arc4.UInt64
    accrued_rewards : arc4.UInt64
    is_funded : bool
main totem
# gritty pewter ```class Stake(arc4.Struct, kw_only=True): owner_address : Account stake...

I believe I've got what you need working like so

from algopy import ARC4Contract, arc4, Account, BoxMap, Txn
from algopy.arc4 import abimethod

class Stake(arc4.Struct):
    owner_address : arc4.Address
    stake_amount : arc4.UInt64
    stake_period_in_days : arc4.UInt64
    stake_apy : arc4.UInt64
    nft_asset_id : arc4.UInt64
    initial_block_no : arc4.UInt64
    last_paid_at_ts : arc4.UInt64
    accrued_rewards : arc4.UInt64
    is_funded : arc4.Bool

class Vrf(ARC4Contract):
    def __init__(self) -> None:
        self.box_map = BoxMap(Account, Stake, key_prefix="")

    @abimethod()
    def struct_in_box(self, struct: Stake) -> None:
        self.box_map[Txn.sender] = struct.copy()
gritty pewter
#

sure, but...what it still doesn't show is this: if I have an Account, that I want to assign to an instance of Stake.owner_address, how do I convert it?

#

That I still can't figure out.

#

Sure, I can use the Txn.sender as the BoxMap key.

#

I've got that working

#

What I'm trying to figure out how to do is just how to assign an Account to a value within the struct (as an arc4.Address, presumably) such that it will work.

#

And I can't do it directly, and I can't wrap it in a constructor (e.g., arc4.Address(my_account_obj), and I can't do that on the .bytes suffix -- like arc4.Address(my_account_obj.bytes)

#

My question is really very simple: what arc4 data structure can i use to represent an algopy.Account, and how do I convert back and forth between the two?

main totem
#
from algopy import ARC4Contract, arc4, Account, BoxMap, Txn
from algopy.arc4 import abimethod, Address

class Stake(arc4.Struct):
    owner_address : arc4.Address
    stake_amount : arc4.UInt64
    stake_period_in_days : arc4.UInt64
    stake_apy : arc4.UInt64
    nft_asset_id : arc4.UInt64
    initial_block_no : arc4.UInt64
    last_paid_at_ts : arc4.UInt64
    accrued_rewards : arc4.UInt64
    is_funded : arc4.Bool

class Vrf(ARC4Contract):
    def __init__(self) -> None:
        self.box_map = BoxMap(Account, Stake, key_prefix="")

    @abimethod()
    def struct_in_box(self, struct: Stake) -> None:
        new_struct = struct.copy()
        new_struct.owner_address = Address.from_bytes(Txn.sender.bytes)
        self.box_map[Txn.sender] = new_struct.copy()```
gritty pewter
#

Ah, ok. So .from_bytes is the way. And then to go the other way...is there a .native?

gritty pewter
#

Perfect. Thank you. I might end up dropping the owner_address property from this struct as an optimization, but it's useful to have it during development, and I'm sure it's a use-case that will arise again in the future, so it's quite helpful to have a solid grasp of how to go back and forth.

main totem
#

Here comes Xartyx to set me straight 😅

full carbon
#

arc4.Address(some_account)

should work? (On mobile so I can’t check for myself)

main totem
#

Yah

#

Even terser:

    @abimethod()
    def struct_in_box(self, struct: Stake) -> None:
        new_struct = struct.copy()
        new_struct.owner_address = Address(Txn.sender)
        self.box_map[Txn.sender] = new_struct.copy()```
full carbon
#

from_bytes works too of course, but pretty sure we added Account as one of the overloads in arc4.Address

main totem
#

We've now fully optimized the code he won't use KEKW

#

Thank you

gritty pewter
#

😅 I'm now trying to figure out why I thought that I couldn't do that.

#

Let's see...if I can get it to build without errors...I may have misread an earlier set of error messages to believe I couldn't do that

gritty pewter
#

Well...new error: mutable reference to ARC4-encoded value must be copied using .copy() when being assigned to another variable stake = self.stake_map[nft_asset_id] -- @main totem I noticed you used .copy() in your sample code. I guess if I'm just reading from the box values, that's fine. If I'm modifying them...I either need to assign to the members of the original reference, or assign the modified copy back to the BoxMap after I'm done?

main totem
#

There was a discussion about this somewhere recently--let me find

#

The need for .copy() is actually coming from semantic compatibility. Objects in python are passed around by reference, when an object (eg. an array or struct) is mutated in one place, all references to that object now point to the mutated version. All this is managed via the heap. The AVM has a very limited 'heap' (the 256 scratch slots), and puya is not currently using it at all. arc4 structs and arrays are instead stored on the stack. When you create multiple 'references' to the same arc4 value in puya, we add a copy of it to the stack meaning if you updated one reference, the other would not see the updated value. So, in order to have algorand python run with the same semantics - we force a .copy() each time you create a new reference to the same object to match what will actually happen on the AVM.
Having said that, it's obviously not a nice API to have to deal with and as soon as we have a technical solution for simulating reference types on the AVM, we would look to retire the need for copy.

gritty pewter
#

Ok, that makes lots of sense. Thanks for sharing that context. It builds! Now to test! I'm sure I'll find new problems 😂

main totem
#

Woo!

full carbon
#

You can also avoid the .copy() requirement if the struct is indicated as frozen e.g.

class MyStruct(arc4.Struct, frozen=True):
    foo: arc4.UInt64
    bar: arc4.Address

Depending on your usage this might be more convenient

gritty pewter
#

I assume "frozen" == "immutable"? If so, that's super useful to know, possibly not strictly applicable to the use-case discussed above 🙂