Skip to main content

agton

Tour

TVM primitives: Cell, Slice and Builder

from agton.ton import Cell, begin_cell

c1 = (
    begin_cell()
    .store_uint(1337, 32)
    .store_maybe_ref(
        begin_cell()
        .store_coins(200)
        .end_cell()
    )
    .end_cell()
)

boc = c1.to_boc()
c2 = Cell.from_boc(boc)
assert c1.hash() == c2.hash()
assert c1 == c2 # Actually compares Cell.hash() under the hood

cs = c1.begin_parse()
assert cs.load_uint(32) == 1337
inner = cs.load_maybe_ref()
assert inner is not None
cs = inner.begin_parse()
assert cs.load_coins() == 200

print(c1) # full tree
# 33[0000053980] -> {
#         12[1C80]
# }
print(repr(c1)) # Cell(33[0000053980] -> 1 refs)

def count_bits(c: Cell) -> int:
    ans = 0
    for b in c.data:
        ans += b
    for r in c.refs:
        ans += count_bits(r)
    return ans

Address

Addresses in TON are rather complex, and agton mirrors the exact tlb definitions. However agton.ton exports Address that is inteded to be used in most cases. Address is exactly AddrStd, even with anycast:(Maybe Anycast)

# All types related to MsgAddress
from agton.ton import Address, MsgAddress, MsgAddressInt, MsgAddressExt
from agton.ton.types.msg_address import AddrNone, AddrExtern, AddrVar, AddrStd

from agton.ton import StateInit, Cell, begin_cell

Address(-1, bytes([0] * 32)) # Ef8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAU

state_init = StateInit(code=Cell.empty(), data=Cell.empty())
Address(0, state_init.to_cell().hash())

a = Address.parse('UQCBirB-586qkioRmyqPgn9y3_GVaXMl_mA9MBxt2BK__Iyp')

assert isinstance(a, MsgAddressInt)
assert isinstance(a, MsgAddress)
assert not isinstance(a, MsgAddressExt)

b = begin_cell().store_address(a)
assert b.to_slice().load_address() == a

a0: Address       = b.to_slice().load_address()
a1: MsgAddress    = b.to_slice().load_msg_address()
a2: MsgAddressInt = b.to_slice().load_msg_address_int()
# a3: MsgAddressExt = b.to_slice().load_msg_address_ext()
# ^ Will trigger a TlbDeserializationError
# cause it isn't an external address stored in b

print(a)         # EQCBirB-586qkioRmyqPgn9y3_GVaXMl_mA9MBxt2BK__NFs
print(f"{a:n}")  # UQCBirB-586qkioRmyqPgn9y3_GVaXMl_mA9MBxt2BK__Iyp
print(f"{a:t}")  # kQCBirB-586qkioRmyqPgn9y3_GVaXMl_mA9MBxt2BK__Grm
print(f"{a:nt}") # 0QCBirB-586qkioRmyqPgn9y3_GVaXMl_mA9MBxt2BK__Dcj
print(f"{a:r}")  # 0:818ab07ee7ceaa922a119b2a8f827f72dff195697325fe603d301c6dd812bffc
print(a.raw())   # 0:818ab07ee7ceaa922a119b2a8f827f72dff195697325fe603d301c6dd812bffc

TL-B

agton.ton offers a powerful abstract class TlbConstructor. Every class inherited from TlbConstructor must implement three methods

@classmethod
def tag(cls) -> tuple[int, int] | None: ...

@classmethod
def deserialize_fields(cls, s: Slice) -> Self: ...

def serialize_fields(cls, b: Builder) -> Builder: ...

Every TlbConstructor gains a lot of flexibility. Let's look at the example

from dataclasses import dataclass
from typing import Self
from agton.ton import TlbConstructor, Builder, Slice, begin_cell

@dataclass(frozen=True)
class Point(TlbConstructor):
    ''' point#3f x:int64 y:int64 = Point; '''
    x: int
    y: int

    @classmethod
    def tag(cls):
        return 0x3f, 8

    @classmethod
    def deserialize_fields(cls, s: Slice) -> Self:
        x = s.load_int(64)
        y = s.load_int(64)
        return cls(x, y)

    def serialize_fields(self, b: Builder) -> Builder:
        b.store_int(self.x, 64)
        b.store_int(self.y, 64)
        return b

p = Point(5, -10)

cell_with_point = p.to_cell()
assert p == Point.from_cell(cell_with_point)

cell_with_point = begin_cell().store_tlb(p).end_cell() # We can store tlb constructor
p = cell_with_point.begin_parse().load_tlb(Point)      # We can also load tlb constructor

But TlbConstructor is not exactly a TlbType. TlbType is a union of multiple constructors. Let's look at the example of true TlbType

@dataclass(frozen=True)
class Circle(TlbConstructor):
    ''' circle$01 p:Point r:uint64 = Shape; '''
    p: Point
    r: int

    @classmethod
    def tag(cls):
        return 0b01, 2

    @classmethod
    def deserialize_fields(cls, s: Slice) -> Self:
        p = s.load_tlb(Point)
        r = s.load_uint(64)
        return cls(p, r)

    def serialize_fields(self, b: Builder) -> Builder:
        b.store_tlb(self.p)
        b.store_uint(self.r, 64)
        return b

@dataclass(frozen=True)
class Line(TlbConstructor):
    ''' line$10 a:Point b:uint64 = Shape; '''
    a: Point
    b: Point

    @classmethod
    def tag(cls):
        return 0b10, 2

    @classmethod
    def deserialize_fields(cls, s: Slice) -> Self:
        a = s.load_tlb(Point)
        b = s.load_tlb(Point)
        return cls(a, b)

    def serialize_fields(self, b: Builder) -> Builder:
        b.store_tlb(self.a)
        b.store_tlb(self.b)
        return b

Shape = Circle | Line

Here Shape is a true TlbType, and we still can do pretty cool thing with it

from random import randrange as rand

def get_shape() -> Shape:
    p1 = Point(rand(10), rand(10))
    p2 = Point(rand(10), rand(10))
    if rand(2):
        return Circle(p1, rand(20))
    return Line(p1, p2)

circle: Shape = get_shape()
cell_with_shape = begin_cell().store_tlb(circle).end_cell()

We can store tlb union types, cause we know the constructor. But there is a problem, we can't easily load tlb union types, we need to load tlb constructor based on tags

cs.load_tlb(Shape) – this does not work cause we don't know how to deserialize shape, and sadly python does not allow to define static methods on union types, but there is a solution! At least partial...

def shape(s: Slice) -> Shape:
    tag_len = Circle.tag()[1] # 2 bits
    tag = s.preload_uint(tag_len)
    if tag == Circle.tag()[0]:
        return Circle.deserialize(s)
    if tag == Line.tag()[0]:
        return Line.deserialize(s)
    raise ValueError(f'Unknown tag: {tag}')

s: Shape = cell_with_shape.begin_parse().load_tlb(shape) # now we can load a Shape!
match s:
    case Circle(p, r): print(f'Circle at {p} with radius={r}')
    case Line(a, b): print(f'Line from {a} to {b}')

So what exactly Slice.load_tlb does? It's defined like this:

def load_tlb[T: TlbConstructor](self, deserializer: type[T] | Callable[[Slice], T]) -> T:
    if isinstance(deserializer, type):
        cls = cast(type[T], deserializer)
        return cls.deserialize(self)
    else:
        t = deserializer(self)
        return t

deserializer is either a

  1. type[TlbConstructor]
  2. (Slice) -> TlbConstructor

The first case is useful when we know the exact constructor that was used or the tlb type has only one constructor. We used the first case when we did cs.load_tlb(Point), and the second case when we were loading a Shape – we defined (Slice) -> TlbConstructor function

Hashmap

Hashmap are by far the most complex tlb structure, and sadly it is impossible to implement it at agton as true collections of TlbConstructor's (TlbConstructor's do not support ~ operator), but still working with Hashmap is pretty convenient and close to tvm hashmaps.

from agton.ton import HashmapCodec, begin_cell

squares = {x: x * x for x in range(-6, 12, 3)}
# {-6: 36, -3: 9, 0: 0, 3: 9, 6: 36, 9: 81}

hashmap_codec = HashmapCodec().with_int_keys(32).with_uint_values(64)
squares_hashmap = hashmap_codec.encode(squares)
# squares_hashmap is HashmapE type
# HashmapE = Hashmap | None

c = begin_cell().store_hashmap_e(squares_hashmap).end_cell() # Storing is not yet implemented :) But would be
cs = c.begin_parse()
new_squares_hashmap = cs.load_hashmap_e(32) # Loading is implemented
new_squares = hashmap_codec.decode(new_squares_hashmap) 
assert set(squares.items()) == set(new_squares.items())

Also Hashmap has methods, that operates with the canonical hashmap representation (BitString -> Slice)

class Hashmap:
    to_dict(self) -> dict[BitString, Slice]: ...

    @classmethod
    def from_dict(cls, d: dict[BitString, Slice]) -> Hashmap: ...

Contract and Provider

Provider is an interface to implement your own contract provider, it basically defines only two user-facing methods

class Provider(ABC):
    def send_external_message(self, message: Message | Cell | bytes) -> bytes:
    def run_get_method(
        self,
        a: MsgAddressInt,
        method_id: int | str,
        stack: Iterable[TvmValue] | TvmValue | None = None
    ) -> tuple[TvmValue, ...] | TvmValue

agton.ton implements only one provider ToncenterClient for now, but this is the basis for the future sandbox feature

One can also implement EmptyProvider

class EmptyProvider(Provider):
    def send_external_message(...): raise ValueError
    def run_get_method(...): raise ValueError

With that provider all contracts should continue to work as long as contracts do not call any get methods

agton.ton gives you a Contract type. Contract basically holds address of a contract and a provider and gives you convenient methods

Contract.create_internal_message(
    value: int | CurrencyCollection = 0,
    body: Cell = Cell.empty()
    init: StateInit | None = None,
    bounce: bool = True
) -> MessageRelaxed

Contract.create_external_message(
    src: MsgAddressExt = AddrNone(),
    body: Cell = Cell.empty()
) -> Message

Contract.run_get_method(
    method_id: int | str,
    stack: Iterable[TvmValue] | TvmValue | None = None
) -> tuple[TvmValue, ...] | TvmValue

Contract is intended to be used for a wrappers, here is simplest example of it's use case

from dataclasses import dataclass
from typing import Self
from agton.ton import (
    ToncenterClient, Contract,
    TlbConstructor, Address, MessageRelaxed, CurrencyCollection,
    Builder, Slice, to_nano
)

@dataclass(frozen=True)
class Increase(TlbConstructor):
    ''' increase#0978f14e by:uint32 '''
    by: int

    @classmethod
    def tag(cls):
        return 0x0978f14e, 32

    @classmethod
    def deserialize_fields(cls, s: Slice) -> Self:
        by = s.load_uint(32)
        return cls(by)

    def serialize_fields(self, b: Builder) -> Builder:
        b.store_uint(self.by, 32)
        return b


class Counter(Contract):
    def create_increase_message(
                self,
                by: int,
                value: int | CurrencyCollection = to_nano(0.2)
            ) -> MessageRelaxed:
        return self.create_internal_message(
            value=value,
            body=Increase(by).to_cell()
        )
    
    def get_counter(self) -> int:
        s = self.provider.run_get_method(self.address, 'counter')
        match s:
            case int(): return s
            case _: raise TypeError(f'Unexpected result stack for get_counter: {s!r}')


provider = ToncenterClient(net='testnet')
counter_address = Address.parse('I am too lazy to actually deploy counter')
counter = Counter(provider, counter_address)

print(counter.get_counter())
message: MessageRelaxed = counter.create_increase_message(3)
assert message.info.dest == counter.address

Notice that message in the example above is MessageRelaxed, that is an internal message. With message.info.dest equal to conuter.address. We can't simply send it to blockchain, we need to send it via sender somehow. agton.wallet implements different wallets (currently only WalletV3R2) so you can send it via wallets implemented there

Metadata

Release files for agton 0.2.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for agton 0.2.5
File Size Uploaded Provenance
agton-0.2.5.tar.gz 44.4 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for agton 0.2.5
File Interpreter ABI Platform
agton-0.2.5-py3-none-any.whl Python 3 none any Details

Total release size: 102.3 kB

Release files / agton-0.2.5.tar.gz

Download URL agton-0.2.5.tar.gz
Size 44.4 kB
Tags Source
SHA-256 checksum
How to use checksums
ddb1b6b994109e0b61005bf389a8698121fc65cde0905d046764f55f95d47d02
BLAKE2b-256 checksum
How to use checksums
21a63d484ef3f5ffc53488e64d34db8a49388b2dd1caa996085949466ee07689
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.2

Release files / agton-0.2.5-py3-none-any.whl

Download URL agton-0.2.5-py3-none-any.whl
Size 57.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c26e59fd7604eee981ffd4b4521a1470b9f34953b866a847f6603bbcecee1fc5
BLAKE2b-256 checksum
How to use checksums
6460960e1fe90284f86bd4c7fd760dd0ccaeaaa1f45286e81de6e44a8ef351de
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.2

Security for agton 0.2.5

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page