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

Add JsonRef #417

Merged
merged 5 commits into from
Jun 30, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
1 change: 1 addition & 0 deletions pyteal/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ __all__ = [
"InnerTxnGroup",
"Int",
"Itob",
"JsonRef",
"Keccak256",
"LabelReference",
"Le",
Expand Down
2 changes: 2 additions & 0 deletions pyteal/ast/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
# ternary ops
from pyteal.ast.ternaryexpr import Divw, Ed25519Verify, SetBit, SetByte
from pyteal.ast.substring import Substring, Extract, Suffix
from pyteal.ast.jsonref import JsonRef

# more ops
from pyteal.ast.naryexpr import NaryExpr, And, Or, Concat
Expand Down Expand Up @@ -281,4 +282,5 @@
"EcdsaVerify",
"EcdsaDecompress",
"EcdsaRecover",
"JsonRef",
]
77 changes: 77 additions & 0 deletions pyteal/ast/jsonref.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from typing import TYPE_CHECKING
from enum import Enum

from pyteal.types import TealType, require_type
from pyteal.errors import TealTypeError, verifyFieldVersion, verifyTealVersion
from pyteal.ir import TealOp, Op, TealBlock
from pyteal.ast.expr import Expr
from pyteal.ast.leafexpr import LeafExpr

if TYPE_CHECKING:
from pyteal.compiler import CompileOptions


class JsonRefType(Enum):
# fmt: off
# id | name | type | min version
json_string = (0, "JSONString", TealType.bytes, 7)
json_uint64 = (1, "JSONUint64", TealType.uint64, 7)
json_object = (2, "JSONObject", TealType.bytes, 7)
# fmt: on

def __init__(self, id: int, name: str, type: TealType, min_version: int) -> None:
self.id = id
self.arg_name = name
self.ret_type = type
self.min_version = min_version

def type_of(self) -> TealType:
return self.ret_type


JsonRefType.__module__ = "pyteal"


class JsonRef(LeafExpr):
"""An expression that accesses a key from a supported utf-8 encoded json object.

The json object must satisfy a [particular specification](https://github.com/algorand/go-algorand/blob/master/data/transactions/logic/jsonspec.md).
"""

json_string = JsonRefType.json_string
json_uint64 = JsonRefType.json_uint64
json_object = JsonRefType.json_object

def __init__(self, type: JsonRefType, json_obj: Expr, key: Expr) -> None:
super().__init__()

if not isinstance(type, JsonRefType):
raise TealTypeError(type, JsonRefType)
self.type = type

require_type(json_obj, TealType.bytes)
self.json_obj = json_obj

require_type(key, TealType.bytes)
self.key = key

def __teal__(self, options: "CompileOptions"):
verifyTealVersion(
Op.json_ref.min_version,
options.version,
"TEAL version too low to use op json_ref",
)

verifyFieldVersion(self.type.arg_name, self.type.min_version, options.version)

op = TealOp(self, Op.json_ref, self.type.arg_name)
return TealBlock.FromOp(options, op, self.json_obj, self.key)

def __str__(self):
return "(JsonRef {})".format(self.type.arg_name)

def type_of(self):
return self.type.type_of()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like how you had the base64 .std/.url, does it make sense to do something similar here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It very well might. I could easily be convinced. My thinking is that if I saw the following:

JsonRef.string(Bytes('{"a":"b"}'), Bytes("a"))

I wouldn't be sure what the .string property meant at first glance. Without a thorough understanding of the json_ref opcode I wouldn't immediately understand whether string refers to the key type, or the input type, or the value's decoded type.

My hope was that explicitly having to set the format would make it clearer at first glance.

JsonRef(JsonRef.json_string, Bytes('{"a":"b"}'), Bytes("a"))

However, as I'm writing this, I'm thinking that named functions .as_string, .as_uint64, and .as_object would solve both issues. I'll add those 👍

Copy link
Contributor Author

@jdtzmn jdtzmn Jun 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@barnjamin I'm having second thoughts about this. As I work more with the PyTeal library, it seems that the "PyTeal way" of doing things is to use dot notation for accessing properties but passed parameters for encodings or specifying return value types. For example, you see AssetParam.total() to access a property of an asset param, but EcdsaVerify(EcdsaCurve.Secp256k1, *args) for ecdsa verification.

I actually think using dot notation for everything could provide a better developer experience and I would probably be in favor of adopting that method moving forward. However, if we were trying to stay consistent with existing behavior, we should get rid of .std/.url/.as_string/etc in favor of exported enums as function parameters.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My lightly held 2c: I'm on-board with the PR's approach. If another reviewer has a strong opinion otherwise, I can be convinced to revert for consistency.


JsonRef.__module__ = "pyteal"
89 changes: 89 additions & 0 deletions pyteal/ast/jsonref_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import pytest

import pyteal as pt

teal6Options = pt.CompileOptions(version=6)
teal7Options = pt.CompileOptions(version=7)


def test_json_string():
args = [pt.Bytes('{"foo":"bar"}'), pt.Bytes("foo")]
expr = pt.JsonRef(pt.JsonRef.json_string, *args)
assert expr.type_of() == pt.TealType.bytes

expected = pt.TealSimpleBlock(
[
pt.TealOp(args[0], pt.Op.byte, '"{\\"foo\\":\\"bar\\"}"'),
pt.TealOp(args[1], pt.Op.byte, '"foo"'),
pt.TealOp(expr, pt.Op.json_ref, "JSONString"),
]
)

actual, _ = expr.__teal__(teal7Options)
actual.addIncoming()
actual = pt.TealBlock.NormalizeBlocks(actual)

with pt.TealComponent.Context.ignoreExprEquality():
assert actual == expected

with pytest.raises(pt.TealInputError):
expr.__teal__(teal6Options)


def test_json_uint64():
args = [pt.Bytes('{"foo":123456789}'), pt.Bytes("foo")]
expr = pt.JsonRef(pt.JsonRef.json_uint64, *args)
assert expr.type_of() == pt.TealType.uint64

expected = pt.TealSimpleBlock(
[
pt.TealOp(args[0], pt.Op.byte, '"{\\"foo\\":123456789}"'),
pt.TealOp(args[1], pt.Op.byte, '"foo"'),
pt.TealOp(expr, pt.Op.json_ref, "JSONUint64"),
]
)

actual, _ = expr.__teal__(teal7Options)
actual.addIncoming()
actual = pt.TealBlock.NormalizeBlocks(actual)

with pt.TealComponent.Context.ignoreExprEquality():
assert actual == expected

with pytest.raises(pt.TealInputError):
expr.__teal__(teal6Options)


def test_json_object():
args = [pt.Bytes('{"foo":{"key": "value"}}'), pt.Bytes("foo")]
expr = pt.JsonRef(pt.JsonRef.json_object, *args)
assert expr.type_of() == pt.TealType.bytes

expected = pt.TealSimpleBlock(
[
pt.TealOp(args[0], pt.Op.byte, '"{\\"foo\\":{\\"key\\": \\"value\\"}}"'),
pt.TealOp(args[1], pt.Op.byte, '"foo"'),
pt.TealOp(expr, pt.Op.json_ref, "JSONObject"),
]
)

actual, _ = expr.__teal__(teal7Options)
actual.addIncoming()
actual = pt.TealBlock.NormalizeBlocks(actual)

with pt.TealComponent.Context.ignoreExprEquality():
assert actual == expected

with pytest.raises(pt.TealInputError):
expr.__teal__(teal6Options)


def test_json_ref_invalid():
with pytest.raises(pt.TealTypeError):
pt.JsonRef(pt.Bytes("my string"), pt.Bytes("a"), pt.Bytes("a"))

with pytest.raises(pt.TealTypeError):
pt.JsonRef(pt.JsonRef.json_object, pt.Int(0), pt.Bytes("a"))

with pytest.raises(pt.TealTypeError):
pt.JsonRef(pt.JsonRef.json_string, pt.Bytes("a"), pt.Int(0))
1 change: 1 addition & 0 deletions pyteal/ir/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ def min_version(self) -> int:
gitxnas = OpType("gitxnas", Mode.Application, 6)
gloadss = OpType("gloadss", Mode.Application, 6)
acct_params_get = OpType("acct_params_get", Mode.Application, 6)
json_ref = OpType("json_ref", Mode.Signature | Mode.Application, 7)
# fmt: on


Expand Down