Skip to content

Commit 42b0ef3

Browse files
authored
types: support txn.Access transaction constructor (#580)
1 parent 720681d commit 42b0ef3

File tree

3 files changed

+507
-15
lines changed

3 files changed

+507
-15
lines changed

algosdk/app_access.py

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
from collections import OrderedDict
2+
from os import access
3+
from typing import List, Optional, Tuple, Union
4+
5+
from algosdk import encoding
6+
from algosdk.box_reference import BoxReference
7+
8+
9+
def translate_to_resource_references(
10+
app_id: int,
11+
accounts: Optional[List[str]] = None,
12+
foreign_assets: Optional[List[int]] = None,
13+
foreign_apps: Optional[List[int]] = None,
14+
boxes: Optional[List[Tuple[int, bytes]]] = None,
15+
holdings: Optional[List[Tuple[int, str]]] = None,
16+
locals: Optional[List[Tuple[int, str]]] = None,
17+
) -> List["ResourceReference"]:
18+
"""
19+
Convert accounts, apps, assets, boxes, holdings, locals into list of ResourceReference
20+
that is suitable for txn.Access field creation.
21+
22+
Args:
23+
app_id (int): current application id
24+
accounts (list[string], optional): list of additional accounts involved in call
25+
foreign_apps (list[int], optional): list of other applications (identified by index) involved in call
26+
foreign_assets (list[int], optional): list of assets involved in call
27+
boxes(list[(int, bytes)], optional): list of tuples specifying app id and key for boxes the app may access
28+
holdings (list[int, str], optional): lists of tuples specifying the asset holdings to be accessed during evaluation of the application;
29+
zero (empty) address means sender
30+
locals (list[int, str], optional): lists of tuples specifying the local states to be accessed during evaluation of the application;
31+
zero (empty) address means sender
32+
"""
33+
access: List["ResourceReference"] = []
34+
35+
def ensure(target: "ResourceReference") -> int:
36+
for idx, a in enumerate(access):
37+
if (
38+
a.address == target.address
39+
and a.asset_id == target.asset_id
40+
and a.app_id == target.app_id
41+
):
42+
return idx + 1
43+
access.append(target)
44+
return len(access)
45+
46+
for account in accounts or []:
47+
ensure(ResourceReference(address=account))
48+
49+
for asset in foreign_assets or []:
50+
ensure(ResourceReference(asset_id=asset))
51+
52+
for app in foreign_apps or []:
53+
ensure(ResourceReference(app_id=app))
54+
55+
for asset, addr in holdings or []:
56+
addr_idx = 0
57+
if addr:
58+
addr_idx = ensure(ResourceReference(address=addr))
59+
asset_idx = ensure(ResourceReference(asset_id=asset))
60+
access.append(
61+
ResourceReference(
62+
holding_reference=HoldingRef(
63+
asset_index=asset_idx, addr_index=addr_idx
64+
)
65+
)
66+
)
67+
68+
for app, addr in locals or []:
69+
app_idx = 0
70+
if app and app != app_id:
71+
app_idx = ensure(ResourceReference(app_id=app))
72+
addr_idx = 0
73+
if addr:
74+
addr_idx = ensure(ResourceReference(address=addr))
75+
access.append(
76+
ResourceReference(
77+
locals_reference=LocalsRef(
78+
app_index=app_idx, addr_index=addr_idx
79+
)
80+
)
81+
)
82+
83+
for app, name in boxes or []:
84+
app_idx = 0
85+
if app and app != app_id:
86+
app_idx = ensure(ResourceReference(app_id=app))
87+
access.append(
88+
ResourceReference(
89+
box_reference=BoxReference(app_index=app_idx, name=name)
90+
)
91+
)
92+
93+
return access
94+
95+
96+
class ResourceReference:
97+
def __init__(
98+
self,
99+
address: Optional[str] = None,
100+
asset_id: Optional[int] = None,
101+
app_id: Optional[int] = None,
102+
box_reference: Optional[BoxReference] = None,
103+
holding_reference: Optional["HoldingRef"] = None,
104+
locals_reference: Optional["LocalsRef"] = None,
105+
):
106+
self.app_id = app_id
107+
self.address = address
108+
self.asset_id = asset_id
109+
self.box_reference = box_reference
110+
self.holding_reference = holding_reference
111+
self.locals_reference = locals_reference
112+
113+
def dictify(self):
114+
d = dict()
115+
if self.address:
116+
d["d"] = encoding.decode_address(self.address)
117+
if self.asset_id:
118+
d["s"] = self.asset_id
119+
if self.app_id:
120+
d["p"] = self.app_id
121+
if self.box_reference:
122+
d["b"] = self.box_reference.dictify()
123+
if self.holding_reference:
124+
d["h"] = self.holding_reference.dictify()
125+
if self.locals_reference:
126+
d["l"] = self.locals_reference.dictify()
127+
od = OrderedDict(sorted(d.items()))
128+
return od
129+
130+
@staticmethod
131+
def undictify(d):
132+
return ResourceReference(
133+
address=encoding.encode_address(d["d"]) if "d" in d else "",
134+
asset_id=d["s"] if "s" in d else None,
135+
app_id=d["p"] if "p" in d else None,
136+
box_reference=BoxReference.undictify(d["b"]) if "b" in d else None,
137+
holding_reference=(
138+
HoldingRef.undictify(d["h"]) if "h" in d else None
139+
),
140+
locals_reference=LocalsRef.undictify(d["l"]) if "l" in d else None,
141+
)
142+
143+
def __eq__(self, value):
144+
if not isinstance(value, ResourceReference):
145+
return False
146+
return (
147+
self.address == value.address
148+
and self.asset_id == value.asset_id
149+
and self.app_id == value.app_id
150+
and self.box_reference == value.box_reference
151+
and self.holding_reference == value.holding_reference
152+
and self.locals_reference == value.locals_reference
153+
)
154+
155+
156+
class LocalsRef:
157+
"""
158+
Represents a local reference in txn.Access with an app index and address index.
159+
160+
Args:
161+
app_index (int): index of the application in the access array
162+
addr_index (int): index of the address in the access array
163+
"""
164+
165+
def __init__(self, app_index: int, addr_index: int):
166+
self.app_index = app_index
167+
self.addr_index = addr_index
168+
169+
def dictify(self):
170+
d = dict()
171+
if self.app_index:
172+
d["p"] = self.app_index
173+
if self.addr_index:
174+
d["d"] = self.addr_index
175+
od = OrderedDict(sorted(d.items()))
176+
return od
177+
178+
@staticmethod
179+
def undictify(d):
180+
return LocalsRef(
181+
d["p"] if "p" in d else 0,
182+
d["d"] if "d" in d else 0,
183+
)
184+
185+
def __eq__(self, other):
186+
if not isinstance(other, LocalsRef):
187+
return False
188+
return (
189+
self.app_index == other.app_index
190+
and self.addr_index == other.addr_index
191+
)
192+
193+
194+
class HoldingRef:
195+
"""
196+
Represents a holding reference in txn.Access with an asset index and address index.
197+
198+
Args:
199+
asset_index (int): index of the asset in the access array
200+
addr_index (int): index of the address in the access array
201+
"""
202+
203+
def __init__(self, asset_index: int, addr_index: int):
204+
self.asset_index = asset_index
205+
self.addr_index = addr_index
206+
207+
def dictify(self):
208+
d = dict()
209+
if self.asset_index:
210+
d["s"] = self.asset_index
211+
if self.addr_index:
212+
d["d"] = self.addr_index
213+
od = OrderedDict(sorted(d.items()))
214+
return od
215+
216+
@staticmethod
217+
def undictify(d):
218+
return HoldingRef(
219+
d["s"] if "s" in d else 0,
220+
d["d"] if "d" in d else 0,
221+
)
222+
223+
def __eq__(self, other):
224+
if not isinstance(other, HoldingRef):
225+
return False
226+
return (
227+
self.asset_index == other.asset_index
228+
and self.addr_index == other.addr_index
229+
)

algosdk/transaction.py

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22
import binascii
33
import msgpack
44
from enum import IntEnum
5-
from typing import List, Union, Optional, cast
5+
from typing import cast, List, Optional, Tuple, Union
66
from collections import OrderedDict
77

88
from algosdk import account, constants, encoding, error, logic
99
from algosdk.box_reference import BoxReference
10+
from algosdk.app_access import (
11+
translate_to_resource_references,
12+
ResourceReference,
13+
)
1014
from algosdk.v2client import algod, models
1115
from nacl.exceptions import BadSignatureError
1216
from nacl.signing import SigningKey, VerifyKey
@@ -1555,6 +1559,11 @@ class ApplicationCallTxn(Transaction):
15551559
foreign_assets (list[int], optional): list of assets involved in call
15561560
extra_pages (int, optional): additional program space for supporting larger programs. A page is 1024 bytes.
15571561
boxes(list[(int, bytes)], optional): list of tuples specifying app id and key for boxes the app may access
1562+
use_access (bool, optional): whether to use access lists for the application
1563+
holdings (list[int, str], optional): lists of tuples specifying the asset holdings to be accessed during evaluation of the application;
1564+
zero (empty) address means sender
1565+
locals (list[int, str], optional): lists of tuples specifying the local states to be accessed during evaluation of the application;
1566+
zero (empty) address means sender
15581567
15591568
Attributes:
15601569
sender (str)
@@ -1574,6 +1583,7 @@ class ApplicationCallTxn(Transaction):
15741583
foreign_assets (list[int])
15751584
extra_pages (int)
15761585
boxes (list[(int, bytes)])
1586+
resources (list[ResourceReference])
15771587
"""
15781588

15791589
def __init__(
@@ -1595,6 +1605,10 @@ def __init__(
15951605
rekey_to=None,
15961606
extra_pages=0,
15971607
boxes=None,
1608+
use_access=None,
1609+
holdings=None,
1610+
locals=None,
1611+
resources=None,
15981612
):
15991613
Transaction.__init__(
16001614
self, sender, sp, note, lease, constants.appcall_txn, rekey_to
@@ -1606,13 +1620,44 @@ def __init__(
16061620
self.approval_program = self.teal_bytes(approval_program)
16071621
self.clear_program = self.teal_bytes(clear_program)
16081622
self.app_args = self.bytes_list(app_args)
1609-
self.accounts = accounts if accounts else None
1610-
self.foreign_apps = self.int_list(foreign_apps)
1611-
self.foreign_assets = self.int_list(foreign_assets)
16121623
self.extra_pages = extra_pages
1613-
self.boxes = BoxReference.translate_box_references(
1614-
boxes, self.foreign_apps, self.index
1615-
)
1624+
self.accounts: Optional[List[str]] = None
1625+
self.foreign_apps: Optional[List[int]] = None
1626+
self.foreign_assets: Optional[List[int]] = None
1627+
self.boxes: Optional[List[Tuple[int, bytes]]] = None
1628+
self.resources: Optional[List[ResourceReference]] = None
1629+
1630+
if resources and (
1631+
accounts
1632+
or foreign_apps
1633+
or foreign_assets
1634+
or boxes
1635+
or holdings
1636+
or locals
1637+
):
1638+
raise ValueError(
1639+
"cannot specify both resources and other access fields"
1640+
)
1641+
1642+
if resources:
1643+
self.resources = resources
1644+
elif use_access:
1645+
self.resources = translate_to_resource_references(
1646+
app_id=self.index,
1647+
accounts=accounts,
1648+
foreign_assets=foreign_assets,
1649+
foreign_apps=foreign_apps,
1650+
boxes=boxes,
1651+
holdings=holdings,
1652+
locals=locals,
1653+
)
1654+
else:
1655+
self.accounts = accounts if accounts else None
1656+
self.foreign_apps = self.int_list(foreign_apps)
1657+
self.foreign_assets = self.int_list(foreign_assets)
1658+
self.boxes = BoxReference.translate_box_references(
1659+
boxes, self.foreign_apps, self.index
1660+
)
16161661
if not sp.flat_fee:
16171662
mf = constants.min_txn_fee if sp.min_fee is None else sp.min_fee
16181663
self.fee = max(self.estimate_size() * self.fee, mf)
@@ -1664,6 +1709,8 @@ def dictify(self):
16641709
d["apsu"] = self.clear_program
16651710
if self.app_args:
16661711
d["apaa"] = self.app_args
1712+
if self.extra_pages:
1713+
d["apep"] = self.extra_pages
16671714
if self.accounts:
16681715
d["apat"] = [
16691716
encoding.decode_address(account_pubkey)
@@ -1673,10 +1720,10 @@ def dictify(self):
16731720
d["apfa"] = self.foreign_apps
16741721
if self.foreign_assets:
16751722
d["apas"] = self.foreign_assets
1676-
if self.extra_pages:
1677-
d["apep"] = self.extra_pages
16781723
if self.boxes:
16791724
d["apbx"] = [box.dictify() for box in self.boxes]
1725+
if self.resources:
1726+
d["al"] = [ap.dictify() for ap in self.resources]
16801727

16811728
d.update(super(ApplicationCallTxn, self).dictify())
16821729
od = OrderedDict(sorted(d.items()))
@@ -1697,7 +1744,14 @@ def _undictify(d):
16971744
"approval_program": d["apap"] if "apap" in d else None,
16981745
"clear_program": d["apsu"] if "apsu" in d else None,
16991746
"app_args": d["apaa"] if "apaa" in d else None,
1700-
"accounts": d["apat"] if "apat" in d else None,
1747+
"accounts": (
1748+
[
1749+
encoding.encode_address(account_bytes)
1750+
for account_bytes in d["apat"]
1751+
]
1752+
if "apat" in d
1753+
else None
1754+
),
17011755
"foreign_apps": d["apfa"] if "apfa" in d else None,
17021756
"foreign_assets": d["apas"] if "apas" in d else None,
17031757
"extra_pages": d["apep"] if "apep" in d else 0,
@@ -1706,12 +1760,12 @@ def _undictify(d):
17061760
if "apbx" in d
17071761
else None
17081762
),
1763+
"resources": (
1764+
[ResourceReference.undictify(ref) for ref in d["al"]]
1765+
if "al" in d
1766+
else None
1767+
),
17091768
}
1710-
if args["accounts"]:
1711-
args["accounts"] = [
1712-
encoding.encode_address(account_bytes)
1713-
for account_bytes in args["accounts"]
1714-
]
17151769
return args
17161770

17171771
def __eq__(self, other):

0 commit comments

Comments
 (0)