Skip to content

Commit a83c553

Browse files
boblatdaniel-makerx
authored andcommitted
add resource encoding option with value as default
1 parent 1e313e8 commit a83c553

File tree

8 files changed

+676
-186
lines changed

8 files changed

+676
-186
lines changed

src/_algopy_testing/context_helpers/txn_context.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ def defer_app_call(
121121
arc4_signature=arc4_metadata.arc4_signature,
122122
args=ordered_args,
123123
allow_actions=allow_actions,
124+
resource_encoding=arc4_metadata.resource_encoding,
124125
)
125126
# Handle bare methods
126127
else:
@@ -169,7 +170,7 @@ def create_group(
169170
new_group = TransactionGroup(
170171
txns=processed_gtxns,
171172
active_txn_index=active_txn_index,
172-
active_txn_overrides=typing.cast(dict[str, typing.Any], active_txn_overrides),
173+
active_txn_overrides=typing.cast("dict[str, typing.Any]", active_txn_overrides),
173174
)
174175
self._active_group = new_group
175176
try:

src/_algopy_testing/decorators/arc4.py

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
]
3535
)
3636
_CreateValues = typing.Literal["allow", "require", "disallow"]
37+
_ResourceEncoding = typing.Literal["index", "value"]
38+
_Direction = typing.Literal["input", "output"]
3739
ARC4_METADATA_ATTR = "arc4_metadata"
3840

3941

@@ -51,6 +53,7 @@ class MethodMetadata:
5153
create: _CreateValues
5254
allow_actions: Sequence[_AllowActions]
5355
arc4_signature: str | None = None
56+
resource_encoding: _ResourceEncoding = "value"
5457

5558
@property
5659
def is_create(self) -> bool:
@@ -145,6 +148,7 @@ def abimethod( # noqa: PLR0913
145148
name: str | None = None,
146149
create: _CreateValues = "disallow",
147150
allow_actions: Sequence[_AllowActions] = ("NoOp",),
151+
resource_encoding: _ResourceEncoding = "value",
148152
readonly: bool = False,
149153
default_args: Mapping[str, str | object] | None = None,
150154
) -> Callable[[Callable[_P, _R]], Callable[_P, _R]] | Callable[_P, _R]:
@@ -160,13 +164,15 @@ def abimethod( # noqa: PLR0913
160164
allow_actions=allow_actions,
161165
readonly=readonly,
162166
default_args=default_args,
167+
resource_encoding=resource_encoding,
163168
)
164169

165170
arc4_name = name or fn.__name__
166171
metadata = MethodMetadata(
167172
create=create,
168173
allow_actions=allow_actions,
169-
arc4_signature=_generate_arc4_signature_from_fn(fn, arc4_name),
174+
arc4_signature=_generate_arc4_signature_from_fn(fn, arc4_name, resource_encoding),
175+
resource_encoding=resource_encoding,
170176
)
171177
set_arc4_metadata(fn, metadata)
172178

@@ -187,6 +193,7 @@ def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
187193
arc4_signature=metadata.arc4_signature,
188194
args=ordered_args,
189195
allow_actions=allow_actions,
196+
resource_encoding=metadata.resource_encoding,
190197
)
191198
with context.txn._maybe_implicit_txn_group(txns):
192199
check_routing_conditions(app_id, metadata)
@@ -204,6 +211,7 @@ def create_abimethod_txns(
204211
arc4_signature: str,
205212
args: Sequence[object],
206213
allow_actions: Sequence[_AllowActions],
214+
resource_encoding: _ResourceEncoding,
207215
) -> list[algopy.gtxn.TransactionBase]:
208216
contract_app = lazy_context.ledger.get_app(app_id)
209217
txn_fields = get_active_txn_fields(contract_app, allow_actions)
@@ -215,6 +223,7 @@ def create_abimethod_txns(
215223
method_selector=method_selector,
216224
sender=txn_fields["sender"],
217225
app=contract_app,
226+
resource_encoding=resource_encoding,
218227
)
219228
txn_fields.setdefault("accounts", txn_arrays.accounts)
220229
txn_fields.setdefault("assets", txn_arrays.assets)
@@ -273,11 +282,12 @@ class _TxnArrays:
273282
app_args: list[algopy.Bytes]
274283

275284

276-
def _extract_arrays_from_args(
285+
def _extract_arrays_from_args( # noqa: PLR0912
277286
args: Sequence[object],
278287
method_selector: algopy.Bytes,
279288
app: algopy.Application,
280289
sender: algopy.Account,
290+
resource_encoding: _ResourceEncoding,
281291
) -> _TxnArrays:
282292
from _algopy_testing.serialize import native_to_arc4
283293

@@ -291,14 +301,23 @@ def _extract_arrays_from_args(
291301
case _algopy_testing.gtxn.TransactionBase() as txn:
292302
txns.append(txn)
293303
case _algopy_testing.Account() as acc:
294-
app_args.append(_algopy_testing.arc4.UInt8(len(accounts)))
295-
accounts.append(acc)
304+
if resource_encoding == "index":
305+
app_args.append(_algopy_testing.arc4.UInt8(len(accounts)))
306+
accounts.append(acc)
307+
else:
308+
app_args.append(native_to_arc4(acc))
296309
case _algopy_testing.Asset() as asset:
297-
app_args.append(_algopy_testing.arc4.UInt8(len(assets)))
298-
assets.append(asset)
310+
if resource_encoding == "index":
311+
app_args.append(_algopy_testing.arc4.UInt8(len(assets)))
312+
assets.append(asset)
313+
else:
314+
app_args.append(native_to_arc4(asset.id))
299315
case _algopy_testing.Application() as arg_app:
300-
app_args.append(_algopy_testing.arc4.UInt8(len(apps)))
301-
apps.append(arg_app)
316+
if resource_encoding == "index":
317+
app_args.append(_algopy_testing.arc4.UInt8(len(apps)))
318+
apps.append(arg_app)
319+
else:
320+
app_args.append(native_to_arc4(arg_app.id))
302321
case _ as maybe_native:
303322
app_args.append(native_to_arc4(maybe_native))
304323
if len(app_args) > 15:
@@ -313,18 +332,29 @@ def _extract_arrays_from_args(
313332
)
314333

315334

316-
def _generate_arc4_signature_from_fn(fn: typing.Callable[_P, _R], arc4_name: str) -> str:
335+
def _generate_arc4_signature_from_fn(
336+
fn: typing.Callable[_P, _R], arc4_name: str, resource_encoding: _ResourceEncoding
337+
) -> str:
317338
annotations = inspect.get_annotations(fn, eval_str=True).copy()
318-
returns = algosdk.abi.Returns(_type_to_arc4(annotations.pop("return")))
339+
returns = algosdk.abi.Returns(
340+
_type_to_arc4(annotations.pop("return"), resource_encoding, "output")
341+
)
319342
method = algosdk.abi.Method(
320343
name=arc4_name,
321-
args=[algosdk.abi.Argument(_type_to_arc4(a)) for a in annotations.values()],
344+
args=[
345+
algosdk.abi.Argument(_type_to_arc4(a, resource_encoding, "input"))
346+
for a in annotations.values()
347+
],
322348
returns=returns,
323349
)
324350
return method.get_signature()
325351

326352

327-
def _type_to_arc4(annotation: types.GenericAlias | type | None) -> str: # noqa: PLR0911, PLR0912
353+
def _type_to_arc4( # noqa: PLR0912 PLR0911
354+
annotation: types.GenericAlias | type | None,
355+
resource_encoding: _ResourceEncoding,
356+
direction: _Direction,
357+
) -> str:
328358
from _algopy_testing.arc4 import _ABIEncoded
329359
from _algopy_testing.gtxn import Transaction, TransactionBase
330360
from _algopy_testing.models import Account, Application, Asset
@@ -340,7 +370,9 @@ def _type_to_arc4(annotation: types.GenericAlias | type | None) -> str: # noqa:
340370
return "void"
341371

342372
if isinstance(annotation, types.GenericAlias) and typing.get_origin(annotation) is tuple:
343-
tuple_args = [_type_to_arc4(a) for a in typing.get_args(annotation)]
373+
tuple_args = [
374+
_type_to_arc4(a, resource_encoding, direction) for a in typing.get_args(annotation)
375+
]
344376
return f"({','.join(tuple_args)})"
345377

346378
if not isinstance(annotation, type):
@@ -350,11 +382,11 @@ def _type_to_arc4(annotation: types.GenericAlias | type | None) -> str: # noqa:
350382
annotation, Struct
351383
):
352384
tuple_fields = list(inspect.get_annotations(annotation).values())
353-
tuple_args = [_type_to_arc4(a) for a in tuple_fields]
385+
tuple_args = [_type_to_arc4(a, resource_encoding, direction) for a in tuple_fields]
354386
return f"({','.join(tuple_args)})"
355387

356388
if issubclass(annotation, Array | FixedArray | ImmutableArray | ImmutableFixedArray):
357-
return f"{_type_to_arc4(annotation._element_type)}[]"
389+
return f"{_type_to_arc4(annotation._element_type, resource_encoding, direction)}[]"
358390
# arc4 types
359391
if issubclass(annotation, _ABIEncoded):
360392
return annotation._type_info.arc4_name
@@ -365,11 +397,17 @@ def _type_to_arc4(annotation: types.GenericAlias | type | None) -> str: # noqa:
365397
return annotation.type_enum.txn_name
366398
# reference types
367399
if issubclass(annotation, Account):
368-
return "account"
400+
if resource_encoding == "index" and direction == "input":
401+
return "account"
402+
return "address"
369403
if issubclass(annotation, Asset):
370-
return "asset"
404+
if resource_encoding == "index" and direction == "input":
405+
return "asset"
406+
return "uint64"
371407
if issubclass(annotation, Application):
372-
return "application"
408+
if resource_encoding == "index" and direction == "input":
409+
return "application"
410+
return "uint64"
373411
# native types
374412
if issubclass(annotation, UInt64):
375413
return "uint64"

tests/arc4/test_arc4_method_signature.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,3 +369,106 @@ def test_prepare_txns_with_complex(
369369
assert app_args[0] == arc4.arc4_signature(SignaturesContract.complex_sig)
370370
assert result[0].bytes == struct.another_struct.bytes
371371
assert result[1].bytes == struct.bytes
372+
373+
374+
def test_app_args_is_correct_with_index_resource_encoding( # noqa: PLR0913
375+
context: _algopy_testing.AlgopyTestContext,
376+
localnet_creator_address: str,
377+
other_app_id: int,
378+
funded_account: str,
379+
algorand: AlgorandClient,
380+
get_avm_result: AVMInvoker,
381+
) -> None:
382+
# arrange
383+
contract = SignaturesContract()
384+
contract.create()
385+
386+
asa_id = algorand.send.asset_create(
387+
AssetCreateParams(
388+
sender=localnet_creator_address,
389+
total=123,
390+
)
391+
).confirmation[
392+
"asset-index"
393+
] # type: ignore[call-overload]
394+
395+
# act
396+
contract.echo_resource_by_index(
397+
context.any.asset(total=algopy.UInt64(123)),
398+
context.ledger.get_app(other_app_id),
399+
context.ledger.get_account(funded_account),
400+
)
401+
result = get_avm_result(
402+
"echo_resource_by_index",
403+
asset=asa_id,
404+
app=other_app_id,
405+
acc=funded_account,
406+
)
407+
408+
# assert
409+
txn = context.txn.last_active
410+
app_args = [txn.app_args(i) for i in range(int(txn.num_app_args))]
411+
assert app_args == [
412+
algosdk.abi.Method.from_signature(
413+
"echo_resource_by_index(asset,application,account)(uint64,uint64,address)"
414+
).get_selector(),
415+
b"\x00",
416+
b"\x01",
417+
b"\x01",
418+
]
419+
assert app_args[0] == arc4.arc4_signature(SignaturesContract.echo_resource_by_index)
420+
421+
assert result == [asa_id, other_app_id, funded_account]
422+
423+
424+
def test_app_args_is_correct_with_value_resource_encoding( # noqa: PLR0913
425+
context: _algopy_testing.AlgopyTestContext,
426+
localnet_creator_address: str,
427+
other_app_id: int,
428+
funded_account: str,
429+
algorand: AlgorandClient,
430+
get_avm_result: AVMInvoker,
431+
) -> None:
432+
# arrange
433+
contract = SignaturesContract()
434+
contract.create()
435+
436+
asa_id = algorand.send.asset_create(
437+
AssetCreateParams(
438+
sender=localnet_creator_address,
439+
total=123,
440+
)
441+
).confirmation[
442+
"asset-index"
443+
] # type: ignore[call-overload]
444+
445+
asset = context.any.asset(asset_id=asa_id, total=algopy.UInt64(123))
446+
app = context.ledger.get_app(other_app_id)
447+
acc = context.ledger.get_account(funded_account)
448+
# act
449+
contract.echo_resource_by_value(
450+
asset,
451+
app,
452+
acc,
453+
)
454+
result = get_avm_result(
455+
"echo_resource_by_value",
456+
asset=asa_id,
457+
app=other_app_id,
458+
acc=funded_account,
459+
)
460+
461+
# assert
462+
txn = context.txn.last_active
463+
app_args = [txn.app_args(i) for i in range(int(txn.num_app_args))]
464+
assert app_args == [
465+
algosdk.abi.Method.from_signature(
466+
"echo_resource_by_value(uint64,uint64,address)(uint64,uint64,address)"
467+
).get_selector(),
468+
asa_id.to_bytes(length=8), # asset id as bytes
469+
other_app_id.to_bytes(length=8), # app id as bytes
470+
context.ledger.get_account(funded_account).bytes, # account address as bytes
471+
]
472+
assert app_args[0] == arc4.arc4_signature(SignaturesContract.echo_resource_by_value)
473+
474+
assert result == [asset.id, app.id, acc]

tests/artifacts/Arc4ABIMethod/contract.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,14 @@ def with_txn(self, value: arc4.String, pay: gtxn.PaymentTransaction, arr: UInt8A
5151
assert Txn.group_index == 1
5252
assert pay.amount == 123
5353

54-
@arc4.abimethod
54+
@arc4.abimethod(resource_encoding="index")
5555
def with_asset(self, value: arc4.String, asset: Asset, arr: UInt8Array) -> None:
5656
assert value
5757
assert arr
5858
assert asset.total == 123
5959
assert Txn.assets(0) == asset
6060

61-
@arc4.abimethod
61+
@arc4.abimethod(resource_encoding="index")
6262
def with_app(
6363
self, value: arc4.String, app: Application, app_id: arc4.UInt64, arr: UInt8Array
6464
) -> None:
@@ -72,15 +72,15 @@ def with_app(
7272
assert app_txn.apps(1) == app
7373
assert Txn.applications(1) == app
7474

75-
@arc4.abimethod
75+
@arc4.abimethod(resource_encoding="index")
7676
def with_acc(self, value: arc4.String, acc: Account, arr: UInt8Array) -> None:
7777
assert value
7878
assert arr
7979
assert acc.balance == acc.min_balance + 1234
8080
assert Txn.accounts(0) == Txn.sender
8181
assert Txn.accounts(1) == acc
8282

83-
@arc4.abimethod
83+
@arc4.abimethod(resource_encoding="index")
8484
def complex_sig(
8585
self, struct1: MyStruct, txn: algopy.gtxn.Transaction, acc: Account, five: UInt8Array
8686
) -> tuple[MyStructAlias, MyStruct]:
@@ -102,3 +102,31 @@ def complex_sig(
102102
assert five[0] == 5
103103

104104
return struct1.another_struct.copy(), struct1.copy()
105+
106+
@arc4.abimethod(
107+
resource_encoding="index",
108+
)
109+
def echo_resource_by_index(
110+
self, asset: Asset, app: Application, acc: Account
111+
) -> tuple[Asset, Application, Account]:
112+
asset_idx = op.btoi(Txn.application_args(1))
113+
assert asset == Txn.assets(asset_idx), "expected asset to be passed by index"
114+
app_idx = op.btoi(Txn.application_args(2))
115+
assert app == Txn.applications(app_idx), "expected application to be passed by index"
116+
acc_idx = op.btoi(Txn.application_args(3))
117+
assert acc == Txn.accounts(acc_idx), "expected account to be passed by index"
118+
return asset, app, acc
119+
120+
@arc4.abimethod(
121+
resource_encoding="value",
122+
)
123+
def echo_resource_by_value(
124+
self, asset: Asset, app: Application, acc: Account
125+
) -> tuple[Asset, Application, Account]:
126+
asset_id = op.btoi(Txn.application_args(1))
127+
assert asset.id == asset_id, "expected asset to be passed by value"
128+
app_id = op.btoi(Txn.application_args(2))
129+
assert app.id == app_id, "expected application to be passed by value"
130+
address = Txn.application_args(3)
131+
assert acc.bytes == address, "expected account to be passed by value"
132+
return asset, app, acc

0 commit comments

Comments
 (0)