Skip to content

Commit

Permalink
feat: python client library (#1019)
Browse files Browse the repository at this point in the history
* feat: draft python library

* doc: python client

* Update docs/Deku-Canonical/deku_c_python.md

Co-authored-by: Daniel Hines <[email protected]>

* fix: lima support + readme update

* fix: balance for empty data

* fixup! fix: lima support + readme update

Co-authored-by: Daniel Hines <[email protected]>
  • Loading branch information
aguillon and d4hines authored Dec 16, 2022
1 parent 4b9d667 commit ff46602
Show file tree
Hide file tree
Showing 19 changed files with 637 additions and 0 deletions.
7 changes: 7 additions & 0 deletions deku-c/python-client/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
build/*
dist/*
env/*
*.sw?
*/*.sw?
__pycache__
*/__pycache__
5 changes: 5 additions & 0 deletions deku-c/python-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Python Deku Client Library

Please refer to the [online Deku
documentation](https://deku.marigold.dev/docs/Deku-Canonical/deku_c_python) for installation and
usage instructions.
Empty file.
Empty file.
12 changes: 12 additions & 0 deletions deku-c/python-client/deku/core/address.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from typing import TypeAlias

Address: TypeAlias = str


def address_of_dto(yojson_repr):
if yojson_repr[0] == "Originated":
return yojson_repr[1]["contract"]
elif yojson_repr[0] == "Implicit":
return yojson_repr[1]["address"]
else:
raise ValueError(f"Ill-formed DTO: {yojson_repr}")
92 changes: 92 additions & 0 deletions deku-c/python-client/deku/core/operation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from attrs import define, field, validators
from deku.core.address import Address
from deku.core.ticket import TicketID
from typing import Any


@define
class Transfer:
sender: Address = field()
receiver: Address = field()
ticket_id: TicketID = field()
amount: int = field(validator=validators.gt(0))
transaction_type = "TicketTransfer"

def to_dto(self):
return [
"Operation_ticket_transfer",
{
"sender": self.sender,
"receiver": self.receiver,
"ticket_id": self.ticket_id.to_dto(),
"amount": str(self.amount),
},
]


@define
class Withdraw:
sender: Address = field()
owner: Address = field()
ticket_id: TicketID = field()
amount: int = field(validator=validators.gt(0))
transaction_type = "Withdraw"

def address_to_dto(self, address):
if address.startswith("KT"):
return ["Originated", {"contract": self.owner, "entrypoint": None}]
elif address.startswith("tz"):
return ["Implicit", address]
else:
raise ValueError(f"Unsupported address: {address}")

def to_dto(self):
return [
"Operation_withdraw",
{
"sender": self.sender,
"owner": self.address_to_dto(self.owner),
"amount": str(self.amount),
"ticket_id": self.ticket_id.to_dto(),
},
]


@define
class Noop:
sender: Address = field()

def to_dto(self):
return ["Operation_noop", {"sender": self.sender}]


@define
class VMTransaction:
sender: Address = field()
operation: Any = field() # FIXME

def to_dto(self):
return [
"Operation_vm_transaction",
{"sender": self.sender, "operation": self.operation},
]


@define
class HashedOperation:
bytes_: bytes = field()
hash_: bytes = field()
nonce: int = field()
level: int = field()
operation: Any = field # TODO

def to_dto(self):
return [
"Initial_operation",
{
"hash": self.hash_,
"nonce": str(self.nonce),
"level": str(self.level),
"operation": self.operation.to_dto(),
},
]
62 changes: 62 additions & 0 deletions deku-c/python-client/deku/core/proof.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from attrs import define, field, validators
from deku.core.ticket import TicketID
from deku.core.address import Address
from typing import List, Tuple
from deku.utils import b58decode


def bytes_converter(s: str) -> bytes:
if isinstance(s, bytes):
return s
return bytes(s, encoding="utf-8")


def from_str_couples(ls: List[Tuple[str, str]]):
return [(bytes_converter(x), bytes_converter(y)) for (x, y) in ls]


@define
class Handle:
id: int = field()
owner: Address = field()
ticket_id: TicketID = field()
hash: bytes = field(converter=bytes_converter)
amount: int = field(converter=int, validator=validators.gt(0))

def b58decode(self):
assert self.hash.startswith(b"Dq"), (
"Needs to be called on a handle " + "with a b58-encoded hash"
)
hash = b58decode(self.hash)
return Handle(self.id, self.owner, self.ticket_id, hash, self.amount)

def to_dto(self):
return {
"amount": self.amount,
"data": self.ticket_id.data,
"id": self.id,
"owner": self.owner,
"ticketer": self.ticket_id.ticketer,
}


def handle_converter(h) -> Handle:
if isinstance(h, Handle):
return Handle(h.id, h.owner, h.ticket_id, h.hash, h.amount)
return Handle(**h)


@define
class Proof:
withdrawal_handles_hash: bytes = field(converter=bytes_converter)
handle: Handle = field(converter=handle_converter)
proof: List[Tuple[str, str]] = field(converter=from_str_couples)

def b58decode(self):
assert self.withdrawal_handles_hash.startswith(
b"Dq"
), "Needs to be called on a proof with b58-encoded hashes"
withdrawal_handles_hash = b58decode(self.withdrawal_handles_hash)
handle = self.handle.b58decode()
proof = [(b58decode(x), b58decode(y)) for (x, y) in self.proof]
return Proof(withdrawal_handles_hash, handle, proof)
49 changes: 49 additions & 0 deletions deku-c/python-client/deku/core/ticket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from attrs import define, field, validators
from deku.core.address import Address
from typing import Union, Optional


def check_data(self, attribute, value):
return True # TODO


def check_amount(self, attribute, value):
return True # TODO


@define
class TicketID:
ticketer: Address = field()
data: bytes = field(validator=[validators.instance_of(bytes), check_data])

def to_dto(self):
return ["Ticket_id", {"ticketer": self.ticketer, "data": self.data}]

@classmethod
def of_dto(cls, yojson_repr):
assert yojson_repr[0] == "Ticket_id", f"Ill-formed DTO: {yojson_repr}"
return TicketID(
yojson_repr[1]["ticketer"], bytes(yojson_repr[1]["data"], encoding="utf-8")
)


@define(init=False)
class Ticket:
ticket_id: TicketID = field()
amount: int = field()

def __init__(
self,
ticket: Union[TicketID, str],
data_or_amount: Union[bytes, int],
amount: Optional[int] = None,
):
if isinstance(ticket, TicketID):
self.ticket_id = ticket
self.amount = data_or_amount
else:
self.ticket_id = TicketID(ticket, data_or_amount)
self.amount = amount

def with_amount(self, new_amount: int) -> "Ticket":
return Ticket(self.ticket_id, new_amount)
92 changes: 92 additions & 0 deletions deku-c/python-client/deku/deku.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from .network.http import DekuAPI
from .core.operation import Transfer, HashedOperation, Withdraw, VMTransaction
from .core.address import Address
from .core.ticket import TicketID
from pytezos import PyTezosClient
import deku.utils as utils
import random
from typing import Dict, Optional


class DekuChain:
deku_api: DekuAPI
tezos_client: PyTezosClient

def __init__(self, tezos_client=None, deku_api=None):
if deku_api is None:
self.deku_api = DekuAPI("https://deku-canonical-vm0.deku-v1.marigold.dev")
else:
self.deku_api = DekuAPI(deku_api)
self.tezos_client = tezos_client

def self_address(self):
return self.tezos_client.key.public_key_hash()

def info(self) -> Dict:
return self.deku_api.info()

def level(self) -> int:
return self.deku_api.level()

def balance(self, ticket: TicketID, address: Optional[Address] = None) -> int:
if address is None:
address = self.self_address()
return self.deku_api.balance(ticket, address)

def operation_options(self):
level = self.level()
nonce = random.randint(100000, 2**62 - 1) # OCaml max int on 64 bits
return (level, nonce)

# FIXME: right now endpoints are defined in http.py but we already have to
# know the type of their arguments here
def encode_operation(self, level, nonce, operation):
return self.deku_api.encode_operation(
str(nonce), str(level), operation.to_dto()
)

def submit_operation(self, level, nonce, transaction):
bytes_ = self.encode_operation(level, nonce, transaction)
hash_ = utils.b58encode(utils.blake2b(bytes_))
hashed_tx = HashedOperation(bytes_, hash_, nonce, level, transaction)
key = self.tezos_client.key
signature = key.sign(bytes_)
signed_operation_dto = {
"key": key.public_key(),
"signature": signature,
"initial": hashed_tx.to_dto(),
}
return self.deku_api.submit_operation(signed_operation_dto)

# TODO: refactor those methods
def transfer(self, receiver, ticket):
(level, nonce) = self.operation_options() # TODO options
sender = self.self_address()
transaction = Transfer(sender, receiver, ticket.ticket_id, ticket.amount)
return self.submit_operation(level, nonce, transaction)["hash"]

def withdraw(self, tezos_account, ticket):
(level, nonce) = self.operation_options() # TODO options
sender = self.self_address()
withdraw = Withdraw(sender, tezos_account, ticket.ticket_id, ticket.amount)
return self.submit_operation(level, nonce, withdraw)["hash"]

def send_vm_operation(self, vm_operation):
(level, nonce) = self.operation_options()
vm_tx = VMTransaction(self.self_address(), vm_operation)
hash = self.submit_operation(level, nonce, vm_tx)["hash"]
address = self.deku_api.compute_contract_address(hash)
return (hash, address["address"])

def withdraw_proof(self, hash):
return self.deku_api.proof(hash)

def originate(self, source, initial_storage, lang_api):
compiled = lang_api.compile_contract(source, initial_storage)
# TODO: WASM, Ligo
return self.send_vm_operation(compiled)

def invoke(self, contract_address, argument, lang_api):
compiled = lang_api.compile_invocation(contract_address, argument)
# TODO: WASM, Ligo
return self.send_vm_operation(compiled)[0]
Empty file.
Loading

0 comments on commit ff46602

Please sign in to comment.