Skip to content

Commit

Permalink
update multi-call feature
Browse files Browse the repository at this point in the history
  • Loading branch information
laalaguer committed Nov 16, 2021
1 parent 510d2be commit c87ee7d
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 54 deletions.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

setuptools.setup(
name="thor-requests",
version="0.8.1",
version="1.0.0",
author="laalaguer",
author_email="[email protected]",
description="Simple network VeChain SDK for human to interact with the blockchain",
Expand Down
34 changes: 33 additions & 1 deletion tests/test_multi.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
)


def test_multi_vtho(
def test_multi_call_transfer_vtho(
solo_connector, solo_wallet, clean_wallet, vtho_contract, vtho_contract_address
):
c1 = solo_connector.clause(
Expand All @@ -31,3 +31,35 @@ def test_multi_vtho(

for each in responses:
assert each["reverted"] == False
assert each["events"][0]["decoded"] # should have beautified decoded data
assert each["events"][0]["decoded"]["_from"]
assert each["events"][0]["decoded"]["_to"]
assert each["events"][0]["decoded"]["_value"]

def test_multi_call_balance_vtho(
solo_connector, solo_wallet, clean_wallet, vtho_contract, vtho_contract_address
):
c1 = solo_connector.clause(
vtho_contract,
"balanceOf",
[clean_wallet.getAddress()],
vtho_contract_address,
)

c2 = solo_connector.clause(
vtho_contract,
"balanceOf",
[clean_wallet.getAddress()],
vtho_contract_address,
)

responses = solo_connector.call_multi(
solo_wallet.getAddress(),
clauses=[c1, c2],
)

for each in responses:
assert each["reverted"] == False
assert each["decoded"] # should have beautified decoded data
assert each["decoded"]["0"] == 0
assert each["decoded"]["balance"] == 0
76 changes: 76 additions & 0 deletions thor_requests/clause.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'''
Clause is a unique feature in VeChain Thor.
While Ethereum is a single clause transaction, (single-clause-tx)
VeChain can have multiple-clause tx.
'''

from typing import List
from thor_requests.contract import Contract


class Clause:
def __init__(self, to: str, contract: Contract=None, func_name: str=None, func_params: List=None, value: int=0):
'''
There are two types of clause:
1) "Call": function call to a smart contract
Build a clause according to the function name and params.
raise Exception when function is not found by name.
2) "Pure": only transfer of VET:
Set the contract, func_name, and func_params to None
Parameters
----------
to : str
Address of the contract or receiver account.
contract : Contract
To which contract this function belongs.
func_name : str, optional
Name of the function.
func_params : List, optional
Function params supplied by user.
value : int, optional
VET sent with the clause in Wei, by default 0
'''
# input
self.contract = contract
self.func_name = func_name
self.func_params = func_params
self.value = value

self.is_call = contract and func_name
if self.is_call: # Contract call
f = contract.get_function_by_name(func_name, strict_mode=True)
data = f.encode(func_params, to_hex=True)
self.dict = {"to": to, "value": str(value), "data": data}
else: # VET transfer
self.dict = {"to": to, "value": str(value), "data": "0x"}

def is_call(self) -> bool:
'''
If is a smart contract call
Returns
-------
bool
True: is, False: is not
'''
return self.is_call

def get_func_name(self) -> str:
return self.func_name

def get_contract(self) -> Contract:
return self.contract

def to_dict(self) -> dict:
'''
Export clause as a dict.
Returns
-------
dict
The clause as a dict: {"to":, "value":, "data":}
'''
return self.dict
72 changes: 41 additions & 31 deletions thor_requests/connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,30 @@
inject_decoded_event,
inject_decoded_return,
inject_revert_reason,
is_emulate_failed,
read_vm_gases,
build_params,
suggest_gas_for_tx,
)
from .wallet import Wallet
from .contract import Contract
from .clause import Clause
from .const import VTHO_ABI, VTHO_ADDRESS

def _beautify(response:dict, contract:Contract, func_name:str) -> dict:
''' Beautify a emulation response dict, to include decoded return and decoded events '''
# Decode return value
response = inject_decoded_return(response, contract, func_name)
# Decode events (if any)
if not len(response["events"]): # no events just return
return response

response["events"] = [
inject_decoded_event(each_event, contract)
for each_event in response["events"]
]

return response

class Connect:
"""Connect to VeChain"""
Expand Down Expand Up @@ -243,7 +259,7 @@ def emulate(self, emulate_tx_body: dict, block: str = "best") -> List[dict]:
if not (r.status_code == 200):
raise Exception(f"HTTP error: {r.status_code} {r.text}")

all_responses = r.json()
all_responses = r.json() # A list of responses
return list(map(inject_revert_reason, all_responses))

def replay_tx(self, tx_id: str) -> List[dict]:
Expand Down Expand Up @@ -306,7 +322,7 @@ def clause(
func_params: List,
to: str,
value=0,
) -> dict:
) -> Clause:
"""
There are two types of calls:
1) Function call on a smart contract
Expand Down Expand Up @@ -334,12 +350,8 @@ def clause(
dict
The clause as a dict: {"to":, "value":, "data":}
"""
if contract and func_name: # Contract call
f = contract.get_function_by_name(func_name, strict_mode=True)
data = f.encode(func_params, to_hex=True) # Tx clause data
return {"to": to, "value": str(value), "data": data}
else: # VET transfer
return {"to": to, "value": str(value), "data": "0x"}
return Clause(to, contract, func_name, func_params, value)


def call(
self,
Expand All @@ -360,11 +372,11 @@ def call(
Response type view README.md
If function has any return value, it will be included in "decoded" field
"""
# Get the clause object
# Get the Clause object
clause = self.clause(contract, func_name, func_params, to, value)
# Build tx body
tx_body = build_tx_body(
[clause],
[clause.to_dict()],
self.get_chainTag(),
calc_blockRef(self.get_block("best")["id"]),
calc_nonce(),
Expand All @@ -376,25 +388,13 @@ def call(
# Should only have one response, since we only have 1 clause
assert len(e_responses) == 1

# If emulation failed just return the failed.
failed = any_emulate_failed(e_responses)
if failed:
# If emulation failed just return the failed response.
if any_emulate_failed(e_responses):
return e_responses[0]

first_clause = e_responses[0]

# decode the "return data" from the function call
first_clause = inject_decoded_return(first_clause, contract, func_name)
# decode the "event" from the function call
if len(first_clause["events"]):
first_clause["events"] = [
inject_decoded_event(each_event, contract, to)
for each_event in first_clause["events"]
]
return _beautify(e_responses[0], clause.get_contract(), clause.get_func_name())

return first_clause

def call_multi(self, caller: str, clauses: List, gas: int = 0) -> List[dict]:
def call_multi(self, caller: str, clauses: List[Clause], gas: int = 0) -> List[dict]:
"""
Call a contract method (read-only).
This is a single transaction, multi-clause call.
Expand All @@ -406,7 +406,7 @@ def call_multi(self, caller: str, clauses: List, gas: int = 0) -> List[dict]:
"""
# Build tx body
tx_body = build_tx_body(
clauses,
[clause.to_dict() for clause in clauses],
self.get_chainTag(),
calc_blockRef(self.get_block("best")["id"]),
calc_nonce(),
Expand All @@ -417,7 +417,17 @@ def call_multi(self, caller: str, clauses: List, gas: int = 0) -> List[dict]:
e_responses = self.emulate_tx(caller, tx_body)
assert len(e_responses) == len(clauses)

return e_responses
# Try to beautify the responses
_responses = []
for response, clause in zip(e_responses, clauses):
# Failed response just ouput plain response
if is_emulate_failed(response):
_responses.append(response)
continue
# Success response inject beautified decoded data
_responses.append(_beautify(response, clause.get_contract(), clause.get_func_name()))

return _responses

def transact(
self,
Expand Down Expand Up @@ -459,7 +469,7 @@ def transact(
"""
clause = self.clause(contract, func_name, func_params, to, value)
tx_body = build_tx_body(
[clause],
[clause.to_dict()],
self.get_chainTag(),
calc_blockRef(self.get_block("best")["id"]),
calc_nonce(),
Expand Down Expand Up @@ -488,7 +498,7 @@ def transact(
return self.post_tx(encoded_raw)

def transact_multi(
self, wallet: Wallet, clauses: List, gas: int = 0, force: bool = False
self, wallet: Wallet, clauses: List[Clause], gas: int = 0, force: bool = False
):
# Emulate the whole tx first.
e_responses = self.call_multi(wallet.getAddress(), clauses, gas)
Expand All @@ -497,7 +507,7 @@ def transact_multi(

# Build the body
tx_body = build_tx_body(
clauses,
[clause.to_dict() for clause in clauses],
self.get_chainTag(),
calc_blockRef(self.get_block("best")["id"]),
calc_nonce(),
Expand Down
36 changes: 15 additions & 21 deletions thor_requests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
is: boolean functions.
"""

import json

import secrets
from typing import List, Union

Expand Down Expand Up @@ -215,9 +215,7 @@ def calc_revertReason(data: str) -> str:
return None


def inject_decoded_event(
event_dict: dict, contract: Contract, target_address: str = None
) -> dict:
def inject_decoded_event(event_dict: dict, contract: Contract) -> dict:
"""
Inject 'decoded' and 'name' into event
Expand All @@ -232,10 +230,6 @@ def inject_decoded_event(
"data": "0x..."
}
"""
# target_address doesn't compily with event address
if target_address and (event_dict["address"].lower() != target_address.lower()):
return event_dict

e_obj = contract.get_event_by_signature(bytes.fromhex(event_dict["topics"][0][2:]))
if not e_obj: # oops, event not found, cannot decode
return event_dict
Expand All @@ -248,28 +242,28 @@ def inject_decoded_event(
return event_dict


def inject_decoded_return(e_response: dict, contract: Contract, func_name: str) -> dict:
def inject_decoded_return(emulate_response: dict, contract: Contract, func_name: str) -> dict:
"""Inject 'decoded' return value into a emulate response"""
if e_response["reverted"] == True:
return e_response
if emulate_response["reverted"] == True:
return emulate_response

if (not e_response["data"]) or (e_response["data"] == "0x"):
return e_response
if (not emulate_response["data"]) or (emulate_response["data"] == "0x"):
return emulate_response

function_obj = contract.get_function_by_name(func_name, True)
e_response["decoded"] = function_obj.decode(
bytes.fromhex(e_response["data"][2:]) # Remove '0x'
emulate_response["decoded"] = function_obj.decode(
bytes.fromhex(emulate_response["data"][2:]) # Remove '0x'
)

return e_response
return emulate_response


def inject_revert_reason(e_response: dict) -> dict:
"""Inject ['decoded''revertReason'] if the emulate response failed"""
if e_response["reverted"] == True and e_response["data"] != "0x":
e_response["decoded"] = {"revertReason": calc_revertReason(e_response["data"])}
def inject_revert_reason(emulate_response: dict) -> dict:
"""Inject ['decoded']['revertReason'] if the emulate response failed"""
if emulate_response["reverted"] == True and emulate_response["data"] != "0x":
emulate_response["decoded"] = {"revertReason": calc_revertReason(emulate_response["data"])}

return e_response
return emulate_response


def is_reverted(receipt: dict) -> bool:
Expand Down

0 comments on commit c87ee7d

Please sign in to comment.