Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Foreign objects as ABI arguments and address ARC-4 changes #251

Merged
merged 45 commits into from
Dec 28, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
4cab7a8
Start ABI JSON interaction
algochoi Oct 27, 2021
223b05e
Add static annoation
algochoi Oct 27, 2021
bad480d
Fix Method argument parsing
algochoi Oct 27, 2021
3b6f40c
Add ABI Typing to Method arguments
algochoi Oct 29, 2021
7b732a6
Merge remote-tracking branch 'origin/develop' into algochoi/abi-inter…
algochoi Oct 29, 2021
7cf0608
[WIP] Add AtomicTransactionComposer build functions
algochoi Oct 29, 2021
58cef31
[WIP] Sign and send atomic transaction groups
algochoi Nov 1, 2021
43874b2
Add unit tests for object parsing
algochoi Nov 1, 2021
d3fe73d
Clean up method calls
algochoi Nov 3, 2021
c7f4e14
Address PR comments on JSON objects
algochoi Nov 8, 2021
72d172d
Refactor ABI Type to ABIType so it can be exposed to outside world
algochoi Nov 10, 2021
e96b740
Add cucumber steps for ABI tests and update existing implementation s…
algochoi Nov 10, 2021
e052fa7
Refactor TransactionSigner to Abstract class and merge signatures whe…
algochoi Nov 12, 2021
d55f9a6
Update testing to reflect json unit tests and composer tests
algochoi Nov 16, 2021
a748f7b
Formatting and docstring fixes
algochoi Nov 16, 2021
982d519
Add foreign types for method arguments
algochoi Nov 16, 2021
23f63bb
Clean up imports
algochoi Nov 16, 2021
7daa605
Merge branch 'algochoi/abi-interaction' into algochoi/foreign-types
algochoi Nov 16, 2021
f21ec5d
Fix unit test for appId
algochoi Nov 17, 2021
5a6be1c
Merge branch 'algochoi/abi-interaction' into algochoi/foreign-types
algochoi Nov 17, 2021
cfef0a6
Add unit test for foreign array
algochoi Nov 17, 2021
d63b4d1
Refactor some names and add txn as an arg type
algochoi Nov 18, 2021
5c9d28e
Partially address PR comments
algochoi Nov 19, 2021
5e9ab5a
Merge branch 'algochoi/abi-interaction' into algochoi/foreign-types
algochoi Nov 19, 2021
c024249
Fix encoding args for foreign types
algochoi Nov 19, 2021
9fa591a
Add some additional checks for safety
algochoi Nov 19, 2021
351099a
Fix a step so we check for empty string instead of None
algochoi Nov 19, 2021
c5df38c
Correct foreign app and account indices accounting for the implicit a…
algochoi Nov 22, 2021
8f24033
Merge branch 'algochoi/abi-interaction' into algochoi/foreign-types
algochoi Nov 22, 2021
d664879
Merge branch 'develop' into algochoi/foreign-types
algochoi Nov 30, 2021
34c625c
Resolve formatting
algochoi Nov 30, 2021
b29bdaa
Fix unit tests
algochoi Nov 30, 2021
1239547
Fix foreign objects to compact duplicates and special values
algochoi Dec 13, 2021
c218fdf
Refactor foreign objects, transactions, and address some new ABI changes
algochoi Dec 15, 2021
2fbf11a
ABI composer modifications and test updates
algochoi Dec 18, 2021
2503554
Change Interface and Contract to newest ABI changes
algochoi Dec 21, 2021
eed9935
Fix some integration tests for composer
algochoi Dec 21, 2021
00f550b
Fix remaining composer tests
algochoi Dec 21, 2021
0428abe
Formatting changes
algochoi Dec 21, 2021
ad7a483
Fix method json tests
algochoi Dec 22, 2021
fdddcc7
Address PR Comments, clean up and refactor composer and contract
algochoi Dec 28, 2021
8390be2
Create helper function for populating foreign objects
algochoi Dec 28, 2021
9838025
Change type hints on reference and transaction checks
algochoi Dec 28, 2021
909a54d
Add generics and fix dictifying network info
algochoi Dec 28, 2021
90f1daa
Fix step for cucumber test contract parsing
algochoi Dec 28, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions algosdk/abi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@
from algosdk.abi.tuple_type import TupleType
from algosdk.abi.method import Method, Argument, Returns
from algosdk.abi.interface import Interface
from algosdk.abi.contract import Contract
from algosdk.abi.transaction import ABITransactionType, is_abi_transaction_type
from algosdk.abi.contract import Contract, NetworkInfo
from algosdk.abi.transaction import (
ABITransactionType,
is_abi_transaction_type,
check_abi_transaction_type,
)
from algosdk.abi.reference import ABIReferenceType, is_abi_reference_type

name = "abi"
34 changes: 30 additions & 4 deletions algosdk/abi/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ class Contract:
name (string): name of the contract
methods (list): list of Method objects
desc (string, optional): description of the contract
networks (dict, optional): information about the contract in a particular network,
such as app-id.
networks (dict, optional): information about the contract in a
particular network, such as an app-id.
"""

def __init__(
self,
name: str,
methods: List[Method],
desc: str = None,
networks: Dict[str, dict] = None,
networks: Dict[str, "NetworkInfo"] = None,
) -> None:
self.name = name
self.methods = methods
Expand Down Expand Up @@ -56,7 +56,33 @@ def undictify(d: dict) -> "Contract":
name = d["name"]
method_list = [Method.undictify(method) for method in d["methods"]]
desc = d["desc"] if "desc" in d else None
networks = d["networks"] if "networks" in d else None
networks = d["networks"] if "networks" in d else {}
for k, v in networks.items():
networks[k] = NetworkInfo.undictify(v)
return Contract(
name=name, desc=desc, networks=networks, methods=method_list
)


class NetworkInfo:
"""
Represents network information.

Args:
app_id (int): application ID on a particular network
"""

def __init__(self, app_id: int) -> None:
self.app_id = app_id

def __eq__(self, o: object) -> bool:
if not isinstance(o, NetworkInfo):
return False
return self.app_id == o.app_id

def dictify(self) -> dict:
return {"appID": self.app_id}

@staticmethod
def undictify(d: dict) -> "NetworkInfo":
return NetworkInfo(app_id=d["appID"])
5 changes: 4 additions & 1 deletion algosdk/abi/reference.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from typing import Any


class ABIReferenceType:
# Account reference type
ACCOUNT = "account"
Expand All @@ -9,7 +12,7 @@ class ABIReferenceType:
ASSET = "asset"


def is_abi_reference_type(t):
def is_abi_reference_type(t: Any) -> bool:
return t in (
ABIReferenceType.ACCOUNT,
ABIReferenceType.APPLICATION,
Expand Down
11 changes: 10 additions & 1 deletion algosdk/abi/transaction.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from typing import Any

from algosdk import constants
from algosdk.future.transaction import Transaction


class ABITransactionType:
Expand All @@ -24,7 +27,7 @@ class ABITransactionType:
APPL = constants.APPCALL_TXN


def is_abi_transaction_type(t):
def is_abi_transaction_type(t: Any) -> bool:
return t in (
ABITransactionType.ANY,
ABITransactionType.PAY,
Expand All @@ -34,3 +37,9 @@ def is_abi_transaction_type(t):
ABITransactionType.AFRZ,
ABITransactionType.APPL,
)


def check_abi_transaction_type(t: Any, txn: Transaction) -> bool:
if t == ABITransactionType.ANY:
return True
return txn.type and txn.type == t
109 changes: 62 additions & 47 deletions algosdk/atomic_transaction_composer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Any, List, Union

from algosdk import abi, error
from algosdk.abi.address_type import AddressType
from algosdk.future import transaction
from algosdk.v2client import algod

Expand Down Expand Up @@ -33,6 +34,38 @@ class AtomicTransactionComposerStatus(IntEnum):
COMMITTED = 4


def populate_foreign_array(
value_to_add: Any, foreign_array: List[Any], zero_value: Any = None
algochoi marked this conversation as resolved.
Show resolved Hide resolved
) -> int:
"""
Add a value to an application call's foreign array. The addition will be as
compact as possible, and this function will return an index used to
reference `value_to_add` in the `foreign_array`.

Args:
value_to_add: value to add to the array. If the value is already
present, it will not be added again. Instead, the existing index
will be returned.
foreign_array: the existing foreign array. This input may be modified
to append `value_to_add`.
zero_value: If provided, this value indicates two things: the 0 value is
reserved for this array so `foreign_array` must start at index 1;
additionally, if `value_to_add` equals `zero_value`, then
`value_to_add` will not be added to the array and the 0 index will
be returned.
"""
if zero_value and value_to_add == zero_value:
return 0

offset = 0 if not zero_value else 1

if value_to_add in foreign_array:
return foreign_array.index(value_to_add) + offset

foreign_array.append(value_to_add)
return offset + len(foreign_array) - 1


class AtomicTransactionComposer:
"""
Constructs an atomic transaction group which may contain a combination of
Expand Down Expand Up @@ -224,24 +257,27 @@ def add_method_call(
)

# Initialize foreign object maps
if not accounts:
accounts = []
if not foreign_apps:
foreign_apps = []
if not foreign_assets:
foreign_assets = []
accounts = accounts[:] if accounts else []
foreign_apps = foreign_apps[:] if foreign_apps else []
foreign_assets = foreign_assets[:] if foreign_assets else []

app_args = []
raw_values = []
raw_types = []
txn_list = []

# First app arg must be the selector of the method
app_args.append(method.get_selector())

# Iterate through the method arguments and either pack a transaction
# or encode a ABI value.
for i, arg in enumerate(method.args):
if abi.is_abi_transaction_type(arg.type):
if not isinstance(method_args[i], TransactionWithSigner):
if not isinstance(
method_args[i], TransactionWithSigner
) or not abi.check_abi_transaction_type(
arg.type, method_args[i].txn
):
raise error.AtomicTransactionComposerError(
"expected TransactionWithSigner as method argument, but received: {}".format(
method_args[i]
Expand All @@ -252,30 +288,23 @@ def add_method_call(
if abi.is_abi_reference_type(arg.type):
current_type = abi.UintType(8)
if arg.type == abi.ABIReferenceType.ACCOUNT:
account_arg = str(method_args[i])
if account_arg == sender:
current_arg = 0
elif account_arg in accounts:
current_arg = accounts.index(account_arg) + 1
else:
current_arg = len(accounts) + 1
accounts.append(account_arg)
address_type = AddressType()
account_arg = address_type.decode(
address_type.encode(method_args[i])
)
current_arg = populate_foreign_array(
account_arg, accounts, sender
)
elif arg.type == abi.ABIReferenceType.ASSET:
asset_arg = int(method_args[i])
if asset_arg in foreign_assets:
current_arg = foreign_assets.index(asset_arg)
else:
current_arg = len(foreign_assets)
foreign_assets.append(asset_arg)
current_arg = populate_foreign_array(
asset_arg, foreign_assets
)
elif arg.type == abi.ABIReferenceType.APPLICATION:
app_arg = int(method_args[i])
if app_arg == app_id:
current_arg = 0
elif app_arg in foreign_apps:
current_arg = foreign_apps.index(app_arg) + 1
else:
current_arg = len(foreign_apps) + 1
foreign_apps.append(app_arg)
current_arg = populate_foreign_array(
app_arg, foreign_apps, app_id
)
else:
# Shouldn't reach this line unless someone accidentally
# adds another foreign array arg
Expand All @@ -291,29 +320,13 @@ def add_method_call(
raw_types.append(current_type)
raw_values.append(current_arg)

if len(accounts) > self.FOREIGN_ACCOUNT_LIMIT:
raise error.AtomicTransactionComposerError(
"foreign accounts exceed app limit {}".format(
self.FOREIGN_ACCOUNT_LIMIT
)
)
if (
len(accounts) + len(foreign_apps) + len(foreign_assets)
> self.FOREIGN_ARRAY_LIMIT
):
raise error.AtomicTransactionComposerError(
"foreign object array exceeds app limit {}".format(
self.FOREIGN_ARRAY_LIMIT
)
)

# Compact the arguments into a single tuple, if there are more than
# 15 arguments excluding the selector, into the last app arg slot.
if len(raw_types) > self.MAX_APP_ARG_LIMIT - 1:
additional_types = raw_types[14:]
additional_values = raw_values[14:]
raw_types = raw_types[:14]
raw_values = raw_values[:14]
additional_types = raw_types[self.MAX_APP_ARG_LIMIT - 2 :]
additional_values = raw_values[self.MAX_APP_ARG_LIMIT - 2 :]
raw_types = raw_types[: self.MAX_APP_ARG_LIMIT - 2]
raw_values = raw_values[: self.MAX_APP_ARG_LIMIT - 2]
raw_types.append(abi.TupleType(additional_types))
raw_values.append(additional_values)

Expand Down Expand Up @@ -469,6 +482,8 @@ def execute(
)

self.submit(client)
self.status = AtomicTransactionComposerStatus.SUBMITTED

resp = transaction.wait_for_confirmation(
client, self.tx_ids[0], wait_rounds
)
Expand Down
13 changes: 7 additions & 6 deletions test_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
Method,
Interface,
Contract,
NetworkInfo,
)
from algosdk.future import template, transaction
from algosdk.testing import dryrun
Expand Down Expand Up @@ -4274,12 +4275,12 @@ def test_contract(self):
self.assertEqual(
c.networks,
{
"wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=": {
"appID": 1234
},
"SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=": {
"appID": 5678
},
"wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=": NetworkInfo(
app_id=1234
),
"SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=": NetworkInfo(
app_id=5678
),
},
)
self.assertEqual(
Expand Down