genlayer 0.12.4 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +4 -0
- package/CHANGELOG.md +8 -0
- package/dist/index.js +17970 -17628
- package/esbuild.config.dev.js +1 -2
- package/esbuild.config.prod.js +1 -1
- package/eslint.config.js +2 -1
- package/package.json +5 -3
- package/src/commands/contracts/call.ts +23 -33
- package/src/commands/contracts/deploy.ts +109 -33
- package/src/commands/contracts/index.ts +14 -3
- package/src/commands/general/init.ts +0 -1
- package/src/commands/keygen/create.ts +5 -25
- package/src/commands/scaffold/index.ts +16 -0
- package/src/commands/scaffold/new.ts +34 -0
- package/src/index.ts +2 -0
- package/src/lib/accounts/KeypairManager.ts +43 -0
- package/src/lib/actions/BaseAction.ts +34 -7
- package/src/lib/config/simulator.ts +2 -2
- package/templates/default/LICENSE +21 -0
- package/templates/default/README.md +101 -0
- package/templates/default/__init__.py +0 -0
- package/templates/default/app/.env.example +2 -0
- package/templates/default/app/.vscode/extensions.json +3 -0
- package/templates/default/app/README.md +5 -0
- package/templates/default/app/index.html +17 -0
- package/templates/default/app/package-lock.json +4920 -0
- package/templates/default/app/package.json +23 -0
- package/templates/default/app/postcss.config.js +6 -0
- package/templates/default/app/public/favicon.png +0 -0
- package/templates/default/app/src/App.vue +16 -0
- package/templates/default/app/src/components/Address.vue +38 -0
- package/templates/default/app/src/components/BetsScreen.vue +329 -0
- package/templates/default/app/src/logic/FootballBets.js +100 -0
- package/templates/default/app/src/main.js +5 -0
- package/templates/default/app/src/services/genlayer.js +19 -0
- package/templates/default/app/src/style.css +3 -0
- package/templates/default/app/tailwind.config.js +8 -0
- package/templates/default/app/vite.config.js +7 -0
- package/templates/default/config/__init__.py +0 -0
- package/templates/default/config/genlayer_config.py +14 -0
- package/templates/default/contracts/__init__.py +0 -0
- package/templates/default/contracts/football_bets.py +119 -0
- package/templates/default/deploy/deployScript.ts +31 -0
- package/templates/default/package-lock.json +3231 -0
- package/templates/default/package.json +7 -0
- package/templates/default/requirements.txt +6 -0
- package/templates/default/test/__init__.py +0 -0
- package/templates/default/test/football_bets_get_contract_schema_for_code.py +124 -0
- package/templates/default/test/test_football_bet_success_draw.py +108 -0
- package/templates/default/test/test_football_bet_success_win.py +106 -0
- package/templates/default/test/test_football_bet_unsuccess.py +107 -0
- package/templates/default/tools/__init__.py +0 -0
- package/templates/default/tools/accounts.py +5 -0
- package/templates/default/tools/calldata.py +224 -0
- package/templates/default/tools/request.py +134 -0
- package/templates/default/tools/response.py +52 -0
- package/templates/default/tools/structure.py +39 -0
- package/templates/default/tools/transactions.py +28 -0
- package/templates/default/tools/types.py +214 -0
- package/templates/default/tsconfig.json +7 -0
- package/tests/actions/call.test.ts +39 -79
- package/tests/actions/create.test.ts +11 -72
- package/tests/actions/deploy.test.ts +201 -33
- package/tests/actions/new.test.ts +80 -0
- package/tests/commands/call.test.ts +6 -1
- package/tests/commands/deploy.test.ts +12 -1
- package/tests/commands/new.test.ts +68 -0
- package/tests/index.test.ts +4 -0
- package/tests/libs/accounts/KeypairManager.test.ts +110 -0
- package/tests/libs/baseAction.test.ts +40 -0
- package/vitest.config.ts +1 -1
- package/src/lib/accounts/getPrivateKey.ts +0 -21
- package/tests/libs/getPrivateKey.test.ts +0 -96
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# tests/common/request.py
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
import requests
|
|
5
|
+
import time
|
|
6
|
+
from dotenv import load_dotenv
|
|
7
|
+
from eth_account import Account
|
|
8
|
+
import base64
|
|
9
|
+
|
|
10
|
+
from tools.transactions import sign_transaction, encode_transaction_data
|
|
11
|
+
|
|
12
|
+
import tools.calldata as calldata
|
|
13
|
+
|
|
14
|
+
load_dotenv()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def payload(function_name: str, *args) -> dict:
|
|
18
|
+
return {
|
|
19
|
+
"jsonrpc": "2.0",
|
|
20
|
+
"method": function_name,
|
|
21
|
+
"params": [*args],
|
|
22
|
+
"id": 1,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def post_request(
|
|
27
|
+
payload: dict,
|
|
28
|
+
protocol: str = os.environ["RPCPROTOCOL"],
|
|
29
|
+
host: str = os.environ["RPCHOST"],
|
|
30
|
+
port: str = os.environ["RPCPORT"],
|
|
31
|
+
):
|
|
32
|
+
return requests.post(
|
|
33
|
+
protocol + "://" + host + ":" + port + "/api",
|
|
34
|
+
data=json.dumps(payload),
|
|
35
|
+
headers={"Content-Type": "application/json"},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_transaction_by_hash(transaction_hash: str):
|
|
40
|
+
payload_data = payload("eth_getTransactionByHash", transaction_hash)
|
|
41
|
+
raw_response = post_request(payload_data)
|
|
42
|
+
parsed_raw_response = raw_response.json()
|
|
43
|
+
return parsed_raw_response["result"]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_transaction_count(account_address: str):
|
|
47
|
+
payload_data = payload("eth_getTransactionCount", account_address)
|
|
48
|
+
raw_response = post_request(payload_data)
|
|
49
|
+
parsed_raw_response = raw_response.json()
|
|
50
|
+
return parsed_raw_response["result"]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def call_contract_method(
|
|
54
|
+
contract_address: str,
|
|
55
|
+
from_account: Account,
|
|
56
|
+
method_name: str,
|
|
57
|
+
method_args: list,
|
|
58
|
+
):
|
|
59
|
+
encoded_data = encode_transaction_data(
|
|
60
|
+
[calldata.encode({"method": method_name, "args": method_args})]
|
|
61
|
+
)
|
|
62
|
+
method_response = post_request(
|
|
63
|
+
payload(
|
|
64
|
+
"eth_call",
|
|
65
|
+
{
|
|
66
|
+
"to": contract_address,
|
|
67
|
+
"from": from_account.address,
|
|
68
|
+
"data": encoded_data,
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
).json()
|
|
72
|
+
print("method_response", method_response)
|
|
73
|
+
enc_result = method_response["result"]
|
|
74
|
+
return calldata.decode(base64.b64decode(enc_result))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def send_transaction(
|
|
78
|
+
account: Account,
|
|
79
|
+
contract_address: str,
|
|
80
|
+
method_name: str | None,
|
|
81
|
+
method_args: list | None,
|
|
82
|
+
value: int = 0,
|
|
83
|
+
):
|
|
84
|
+
call_data = (
|
|
85
|
+
None
|
|
86
|
+
if method_name is None and method_args is None
|
|
87
|
+
else [calldata.encode({"method": method_name, "args": method_args})]
|
|
88
|
+
)
|
|
89
|
+
nonce = get_transaction_count(account.address)
|
|
90
|
+
signed_transaction = sign_transaction(
|
|
91
|
+
account, call_data, contract_address, value, nonce
|
|
92
|
+
)
|
|
93
|
+
return send_raw_transaction(signed_transaction)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def deploy_intelligent_contract(
|
|
97
|
+
account: Account, contract_code: str, method_args: list
|
|
98
|
+
) -> tuple[str, dict]:
|
|
99
|
+
nonce = get_transaction_count(account.address)
|
|
100
|
+
deploy_data = [
|
|
101
|
+
contract_code,
|
|
102
|
+
calldata.encode({"method": "__init__", "args": method_args}),
|
|
103
|
+
]
|
|
104
|
+
signed_transaction = sign_transaction(account, deploy_data, nonce=nonce)
|
|
105
|
+
result = send_raw_transaction(signed_transaction)
|
|
106
|
+
contract_address = result["data"]["contract_address"]
|
|
107
|
+
return contract_address, result
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def send_raw_transaction(signed_transaction: str):
|
|
111
|
+
payload_data = payload("eth_sendRawTransaction", signed_transaction)
|
|
112
|
+
raw_response = post_request(payload_data)
|
|
113
|
+
call_method_response = raw_response.json()
|
|
114
|
+
print("call_method_response", call_method_response)
|
|
115
|
+
transaction_hash = call_method_response["result"]
|
|
116
|
+
|
|
117
|
+
transaction_response = wait_for_transaction(transaction_hash)
|
|
118
|
+
return transaction_response
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def wait_for_transaction(transaction_hash: str, interval: int = 10, retries: int = 15):
|
|
122
|
+
attempts = 0
|
|
123
|
+
while attempts < retries:
|
|
124
|
+
transaction_response = get_transaction_by_hash(str(transaction_hash))
|
|
125
|
+
print("transaction_response", transaction_response)
|
|
126
|
+
status = transaction_response["status"]
|
|
127
|
+
if status == "FINALIZED":
|
|
128
|
+
return transaction_response
|
|
129
|
+
time.sleep(interval)
|
|
130
|
+
attempts += 1
|
|
131
|
+
|
|
132
|
+
raise TimeoutError(
|
|
133
|
+
f"Transaction {transaction_hash} not finalized after {retries} retries"
|
|
134
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
def assert_dict_struct(data, structure):
|
|
2
|
+
if isinstance(structure, dict):
|
|
3
|
+
assert_is_instance(data, dict)
|
|
4
|
+
for key, value in structure.items():
|
|
5
|
+
assert key in data
|
|
6
|
+
assert_dict_struct(data[key], value)
|
|
7
|
+
elif isinstance(structure, list):
|
|
8
|
+
assert_is_instance(data, list)
|
|
9
|
+
for item in data:
|
|
10
|
+
assert_dict_struct(item, structure[0])
|
|
11
|
+
else:
|
|
12
|
+
assert_is_instance(data, structure)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def assert_is_instance(data, structure):
|
|
16
|
+
assert isinstance(data, structure), f"Expected {structure}, but got {data}"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def assert_dict_exact(data, expected):
|
|
20
|
+
assert data == expected, f"Expected {expected}, but got {data}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def has_error_status(result: dict) -> bool:
|
|
24
|
+
return "error" in result
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def has_success_status(result: dict) -> bool:
|
|
28
|
+
return "error" not in result
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def has_message(result: dict) -> bool:
|
|
32
|
+
return "message" in result
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def has_data(result: dict) -> bool:
|
|
36
|
+
return "data" in result
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def message_is(result: dict, message: dict) -> bool:
|
|
40
|
+
return result["message"] == message
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def data_is(result: dict, data: dict) -> bool:
|
|
44
|
+
return result["data"] == data
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def message_contains(result: dict, message: dict) -> bool:
|
|
48
|
+
return message in result["message"]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def data_contains(result: dict, data: dict) -> bool:
|
|
52
|
+
return data in result["data"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
execute_icontract_function_response_structure = {
|
|
2
|
+
"consensus_data": {
|
|
3
|
+
"final": bool,
|
|
4
|
+
"leader_receipt": {
|
|
5
|
+
"args": list,
|
|
6
|
+
"class_name": str,
|
|
7
|
+
"contract_state": str,
|
|
8
|
+
"eq_outputs": {"leader": dict},
|
|
9
|
+
"error": str | None,
|
|
10
|
+
"execution_result": str,
|
|
11
|
+
"gas_used": int,
|
|
12
|
+
"method": str,
|
|
13
|
+
"mode": str,
|
|
14
|
+
"node_config": {
|
|
15
|
+
"address": str,
|
|
16
|
+
"config": dict,
|
|
17
|
+
"model": str,
|
|
18
|
+
"provider": str,
|
|
19
|
+
"stake": int,
|
|
20
|
+
"plugin": str,
|
|
21
|
+
"plugin_config": dict,
|
|
22
|
+
},
|
|
23
|
+
"vote": str,
|
|
24
|
+
},
|
|
25
|
+
"validators": list,
|
|
26
|
+
"votes": dict,
|
|
27
|
+
},
|
|
28
|
+
"created_at": str,
|
|
29
|
+
"data": {
|
|
30
|
+
"function_args": str, # TODO: can we make this a list?
|
|
31
|
+
"function_name": str,
|
|
32
|
+
},
|
|
33
|
+
"from_address": str,
|
|
34
|
+
"hash": str,
|
|
35
|
+
"status": str,
|
|
36
|
+
"to_address": str,
|
|
37
|
+
"type": int,
|
|
38
|
+
"value": int,
|
|
39
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from eth_account import Account
|
|
2
|
+
from eth_utils import to_hex
|
|
3
|
+
import rlp
|
|
4
|
+
from eth_account._utils.legacy_transactions import Transaction
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def encode_transaction_data(data: list) -> str:
|
|
8
|
+
serialized_data = rlp.encode(data)
|
|
9
|
+
return to_hex(serialized_data)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def sign_transaction(
|
|
13
|
+
account: Account, data: list = None, to: str = None, value: int = 0, nonce: int = 0
|
|
14
|
+
) -> dict:
|
|
15
|
+
transaction = {
|
|
16
|
+
"nonce": nonce,
|
|
17
|
+
"gasPrice": 0,
|
|
18
|
+
"gas": 0,
|
|
19
|
+
"to": to,
|
|
20
|
+
"value": value,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if data is not None:
|
|
24
|
+
encoded_data = encode_transaction_data(data)
|
|
25
|
+
transaction["data"] = encoded_data
|
|
26
|
+
|
|
27
|
+
signed_transaction = Account.sign_transaction(transaction, account.key)
|
|
28
|
+
return to_hex(signed_transaction.raw_transaction)
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Iterable, Optional
|
|
4
|
+
import base64
|
|
5
|
+
|
|
6
|
+
import collections.abc
|
|
7
|
+
|
|
8
|
+
from eth_hash.auto import keccak
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Address:
|
|
12
|
+
SIZE = 20
|
|
13
|
+
|
|
14
|
+
__slots__ = ("_as_bytes", "_as_hex")
|
|
15
|
+
|
|
16
|
+
_as_bytes: bytes
|
|
17
|
+
_as_hex: str | None
|
|
18
|
+
|
|
19
|
+
def __init__(self, val: str | collections.abc.Buffer):
|
|
20
|
+
self._as_hex = None
|
|
21
|
+
if isinstance(val, str):
|
|
22
|
+
if len(val) == 2 + Address.SIZE * 2 and val.startswith("0x"):
|
|
23
|
+
val = bytes.fromhex(val[2:])
|
|
24
|
+
elif len(val) > Address.SIZE:
|
|
25
|
+
val = base64.b64decode(val)
|
|
26
|
+
else:
|
|
27
|
+
val = bytes(val)
|
|
28
|
+
if not isinstance(val, bytes) or len(val) != Address.SIZE:
|
|
29
|
+
raise Exception(f"invalid address {val}")
|
|
30
|
+
self._as_bytes = val
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def as_bytes(self) -> bytes:
|
|
34
|
+
return self._as_bytes
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def as_hex(self) -> str:
|
|
38
|
+
if self._as_hex is None:
|
|
39
|
+
simple = self._as_bytes.hex()
|
|
40
|
+
low_up = keccak(simple.encode("ascii")).hex()
|
|
41
|
+
res = ["0", "x"]
|
|
42
|
+
for i in range(len(simple)):
|
|
43
|
+
if low_up[i] in ["0", "1", "2", "3", "4", "5", "6", "7"]:
|
|
44
|
+
res.append(simple[i])
|
|
45
|
+
else:
|
|
46
|
+
res.append(simple[i].upper())
|
|
47
|
+
self._as_hex = "".join(res)
|
|
48
|
+
return self._as_hex
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def as_b64(self) -> str:
|
|
52
|
+
return str(base64.b64encode(self.as_bytes), encoding="ascii")
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def as_int(self) -> int:
|
|
56
|
+
return int.from_bytes(self._as_bytes, "little", signed=False)
|
|
57
|
+
|
|
58
|
+
def __hash__(self):
|
|
59
|
+
return hash(self._as_bytes)
|
|
60
|
+
|
|
61
|
+
def __lt__(self, r):
|
|
62
|
+
assert isinstance(r, Address)
|
|
63
|
+
return self._as_bytes < r._as_bytes
|
|
64
|
+
|
|
65
|
+
def __le__(self, r):
|
|
66
|
+
assert isinstance(r, Address)
|
|
67
|
+
return self._as_bytes <= r._as_bytes
|
|
68
|
+
|
|
69
|
+
def __eq__(self, r):
|
|
70
|
+
if not isinstance(r, Address):
|
|
71
|
+
return False
|
|
72
|
+
return self._as_bytes == r._as_bytes
|
|
73
|
+
|
|
74
|
+
def __ge__(self, r):
|
|
75
|
+
assert isinstance(r, Address)
|
|
76
|
+
return self._as_bytes >= r._as_bytes
|
|
77
|
+
|
|
78
|
+
def __gt__(self, r):
|
|
79
|
+
assert isinstance(r, Address)
|
|
80
|
+
return self._as_bytes > r._as_bytes
|
|
81
|
+
|
|
82
|
+
def __repr__(self) -> str:
|
|
83
|
+
return "addr#" + "".join(["{:02x}".format(x) for x in self._as_bytes])
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Vote(Enum):
|
|
87
|
+
AGREE = "agree"
|
|
88
|
+
DISAGREE = "disagree"
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def from_string(cls, value: str) -> "Vote":
|
|
92
|
+
try:
|
|
93
|
+
return cls(value.lower())
|
|
94
|
+
except ValueError:
|
|
95
|
+
raise ValueError(f"Invalid vote value: {value}")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ExecutionMode(Enum):
|
|
99
|
+
LEADER = "leader"
|
|
100
|
+
VALIDATOR = "validator"
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def from_string(cls, value: str) -> "ExecutionMode":
|
|
104
|
+
try:
|
|
105
|
+
return cls(value.lower())
|
|
106
|
+
except ValueError:
|
|
107
|
+
raise ValueError(f"Invalid execution mode value: {value}")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ExecutionResultStatus(Enum):
|
|
111
|
+
SUCCESS = "SUCCESS"
|
|
112
|
+
ERROR = "ERROR"
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def from_string(cls, value: str) -> "ExecutionResultStatus":
|
|
116
|
+
try:
|
|
117
|
+
return cls(value.upper())
|
|
118
|
+
except ValueError:
|
|
119
|
+
raise ValueError(f"Invalid execution result status value: {value}")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class PendingTransaction:
|
|
124
|
+
address: str # Address of the contract to call
|
|
125
|
+
calldata: bytes
|
|
126
|
+
code: bytes | None
|
|
127
|
+
salt_nonce: int
|
|
128
|
+
|
|
129
|
+
def is_deploy(self) -> bool:
|
|
130
|
+
return self.code is not None
|
|
131
|
+
|
|
132
|
+
def to_dict(self):
|
|
133
|
+
if self.code is None:
|
|
134
|
+
return {
|
|
135
|
+
"address": self.address,
|
|
136
|
+
"calldata": str(base64.b64encode(self.calldata), encoding="ascii"),
|
|
137
|
+
}
|
|
138
|
+
else:
|
|
139
|
+
return {
|
|
140
|
+
"code": str(base64.b64encode(self.code), encoding="ascii"),
|
|
141
|
+
"calldata": str(base64.b64encode(self.calldata), encoding="ascii"),
|
|
142
|
+
"salt_nonce": self.salt_nonce,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def from_dict(cls, input: dict) -> "PendingTransaction":
|
|
147
|
+
if "code" in input:
|
|
148
|
+
return cls(
|
|
149
|
+
address="0x",
|
|
150
|
+
calldata=base64.b64decode(input["calldata"]),
|
|
151
|
+
code=base64.b64decode(input["code"]),
|
|
152
|
+
salt_nonce=input.get("salt_nonce", 0),
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
return cls(
|
|
156
|
+
address=input["address"],
|
|
157
|
+
calldata=base64.b64decode(input["calldata"]),
|
|
158
|
+
code=None,
|
|
159
|
+
salt_nonce=0,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class Receipt:
|
|
165
|
+
result: bytes
|
|
166
|
+
calldata: bytes
|
|
167
|
+
gas_used: int
|
|
168
|
+
mode: ExecutionMode
|
|
169
|
+
contract_state: dict[str, dict[str, str]]
|
|
170
|
+
node_config: dict
|
|
171
|
+
eq_outputs: dict[int, str]
|
|
172
|
+
execution_result: ExecutionResultStatus
|
|
173
|
+
vote: Optional[Vote] = None
|
|
174
|
+
pending_transactions: Iterable[PendingTransaction] = ()
|
|
175
|
+
|
|
176
|
+
def to_dict(self):
|
|
177
|
+
return {
|
|
178
|
+
"vote": self.vote.value,
|
|
179
|
+
"execution_result": self.execution_result.value,
|
|
180
|
+
"result": base64.b64encode(self.result).decode("ascii"),
|
|
181
|
+
"calldata": str(base64.b64encode(self.calldata), encoding="ascii"),
|
|
182
|
+
"gas_used": self.gas_used,
|
|
183
|
+
"mode": self.mode.value,
|
|
184
|
+
"contract_state": self.contract_state,
|
|
185
|
+
"node_config": self.node_config,
|
|
186
|
+
"eq_outputs": self.eq_outputs,
|
|
187
|
+
"pending_transactions": [
|
|
188
|
+
pending_transaction.to_dict()
|
|
189
|
+
for pending_transaction in self.pending_transactions
|
|
190
|
+
],
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@classmethod
|
|
194
|
+
def from_dict(cls, input: dict) -> Optional["Receipt"]:
|
|
195
|
+
if input:
|
|
196
|
+
return cls(
|
|
197
|
+
vote=Vote.from_string(input.get("vote")),
|
|
198
|
+
execution_result=ExecutionResultStatus.from_string(
|
|
199
|
+
input.get("execution_result")
|
|
200
|
+
),
|
|
201
|
+
result=base64.b64decode(input.get("result")),
|
|
202
|
+
calldata=base64.b64decode(input.get("calldata")),
|
|
203
|
+
gas_used=input.get("gas_used"),
|
|
204
|
+
mode=ExecutionMode.from_string(input.get("mode")),
|
|
205
|
+
contract_state=input.get("contract_state"),
|
|
206
|
+
node_config=input.get("node_config"),
|
|
207
|
+
eq_outputs={int(k): v for k, v in input.get("eq_outputs", {}).items()},
|
|
208
|
+
pending_transactions=[
|
|
209
|
+
PendingTransaction.from_dict(pending_transaction)
|
|
210
|
+
for pending_transaction in input.get("pending_transactions", [])
|
|
211
|
+
],
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
return None
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import { describe, test, vi, beforeEach, afterEach, expect } from "vitest";
|
|
2
2
|
import { createClient, createAccount } from "genlayer-js";
|
|
3
|
-
import { CallAction
|
|
4
|
-
import { getPrivateKey } from "../../src/lib/accounts/getPrivateKey";
|
|
3
|
+
import { CallAction } from "../../src/commands/contracts/call";
|
|
5
4
|
|
|
6
5
|
vi.mock("genlayer-js");
|
|
7
|
-
vi.mock("../../src/lib/accounts/getPrivateKey");
|
|
8
6
|
|
|
9
|
-
describe("
|
|
10
|
-
let
|
|
7
|
+
describe("CallAction", () => {
|
|
8
|
+
let callActions: CallAction;
|
|
11
9
|
const mockClient = {
|
|
12
10
|
readContract: vi.fn(),
|
|
13
11
|
writeContract: vi.fn(),
|
|
14
12
|
waitForTransactionReceipt: vi.fn(),
|
|
15
|
-
getContractSchema: vi.fn()
|
|
13
|
+
getContractSchema: vi.fn(),
|
|
16
14
|
};
|
|
17
15
|
|
|
18
16
|
const mockPrivateKey = "mocked_private_key";
|
|
@@ -21,8 +19,13 @@ describe("Call Action", () => {
|
|
|
21
19
|
vi.clearAllMocks();
|
|
22
20
|
vi.mocked(createClient).mockReturnValue(mockClient as any);
|
|
23
21
|
vi.mocked(createAccount).mockReturnValue({ privateKey: mockPrivateKey } as any);
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
callActions = new CallAction();
|
|
23
|
+
vi.spyOn(callActions as any, "getPrivateKey").mockResolvedValue(mockPrivateKey);
|
|
24
|
+
|
|
25
|
+
vi.spyOn(callActions as any, "startSpinner").mockImplementation(() => {});
|
|
26
|
+
vi.spyOn(callActions as any, "succeedSpinner").mockImplementation(() => {});
|
|
27
|
+
vi.spyOn(callActions as any, "failSpinner").mockImplementation(() => {});
|
|
28
|
+
vi.spyOn(callActions as any, "log").mockImplementation(() => {});
|
|
26
29
|
});
|
|
27
30
|
|
|
28
31
|
afterEach(() => {
|
|
@@ -30,15 +33,13 @@ describe("Call Action", () => {
|
|
|
30
33
|
});
|
|
31
34
|
|
|
32
35
|
test("calls readContract successfully", async () => {
|
|
33
|
-
const options:
|
|
34
|
-
args: [1, 2, "Hello"]
|
|
35
|
-
};
|
|
36
|
+
const options = { args: [1, 2, "Hello"] };
|
|
36
37
|
const mockResult = "mocked_result";
|
|
37
38
|
|
|
39
|
+
vi.mocked(mockClient.getContractSchema).mockResolvedValue({ methods: { getData: { readonly: true } } });
|
|
38
40
|
vi.mocked(mockClient.readContract).mockResolvedValue(mockResult);
|
|
39
|
-
vi.mocked(mockClient.getContractSchema).mockResolvedValue({methods: {getData: {readonly: true}}});
|
|
40
41
|
|
|
41
|
-
await
|
|
42
|
+
await callActions.call({
|
|
42
43
|
contractAddress: "0xMockedContract",
|
|
43
44
|
method: "getData",
|
|
44
45
|
...options,
|
|
@@ -49,21 +50,19 @@ describe("Call Action", () => {
|
|
|
49
50
|
functionName: "getData",
|
|
50
51
|
args: [1, 2, "Hello"],
|
|
51
52
|
});
|
|
52
|
-
expect(
|
|
53
|
+
expect(callActions["succeedSpinner"]).toHaveBeenCalledWith("Read operation successfully executed", "mocked_result");
|
|
53
54
|
});
|
|
54
55
|
|
|
55
56
|
test("calls writeContract successfully", async () => {
|
|
56
|
-
const options:
|
|
57
|
-
args: [42, "Update"]
|
|
58
|
-
};
|
|
57
|
+
const options = { args: [42, "Update"] };
|
|
59
58
|
const mockHash = "0xMockedTransactionHash";
|
|
60
59
|
const mockReceipt = { status: "success" };
|
|
61
60
|
|
|
61
|
+
vi.mocked(mockClient.getContractSchema).mockResolvedValue({ methods: { updateData: { readonly: false } } });
|
|
62
62
|
vi.mocked(mockClient.writeContract).mockResolvedValue(mockHash);
|
|
63
63
|
vi.mocked(mockClient.waitForTransactionReceipt).mockResolvedValue(mockReceipt);
|
|
64
|
-
vi.mocked(mockClient.getContractSchema).mockResolvedValue({methods: {updateData: {readonly: false}}});
|
|
65
64
|
|
|
66
|
-
await
|
|
65
|
+
await callActions.call({
|
|
67
66
|
contractAddress: "0xMockedContract",
|
|
68
67
|
method: "updateData",
|
|
69
68
|
...options,
|
|
@@ -75,72 +74,33 @@ describe("Call Action", () => {
|
|
|
75
74
|
args: [42, "Update"],
|
|
76
75
|
value: 0n,
|
|
77
76
|
});
|
|
78
|
-
expect(
|
|
79
|
-
|
|
80
|
-
retries: 15,
|
|
81
|
-
interval: 2000,
|
|
82
|
-
});
|
|
83
|
-
expect(mockClient.writeContract).toHaveResolvedWith(mockHash);
|
|
77
|
+
expect(callActions["log"]).toHaveBeenCalledWith("Write transaction hash:", mockHash);
|
|
78
|
+
expect(callActions["succeedSpinner"]).toHaveBeenCalledWith("Write operation successfully executed", mockReceipt);
|
|
84
79
|
});
|
|
85
80
|
|
|
86
|
-
test("
|
|
87
|
-
|
|
88
|
-
args: []
|
|
89
|
-
};
|
|
81
|
+
test("fails when method is not found", async () => {
|
|
82
|
+
vi.mocked(mockClient.getContractSchema).mockResolvedValue({ methods: { updateData: { readonly: false } } });
|
|
90
83
|
|
|
91
|
-
|
|
84
|
+
await callActions.call({ contractAddress: "0xMockedContract", method: "getData", args: [] });
|
|
92
85
|
|
|
93
|
-
|
|
94
|
-
caller.call({
|
|
95
|
-
contractAddress: "0xMockedContract",
|
|
96
|
-
method: "getData",
|
|
97
|
-
...options,
|
|
98
|
-
})
|
|
99
|
-
).rejects.toThrowError('process.exit unexpectedly called with "1"');
|
|
100
|
-
|
|
101
|
-
expect(mockClient.readContract).not.toHaveBeenCalled();
|
|
102
|
-
expect(mockClient.writeContract).not.toHaveBeenCalled();
|
|
86
|
+
expect(callActions["failSpinner"]).toHaveBeenCalledWith("method getData not found.");
|
|
103
87
|
});
|
|
104
88
|
|
|
105
|
-
test("handles errors
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
new Error("Mocked read error")
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
await expect(
|
|
116
|
-
caller.call({
|
|
117
|
-
contractAddress: "0xMockedContract",
|
|
118
|
-
method: "getData",
|
|
119
|
-
...options,
|
|
120
|
-
})
|
|
121
|
-
).rejects.toThrowError("Mocked read error");
|
|
122
|
-
|
|
123
|
-
expect(mockClient.readContract).toHaveBeenCalled();
|
|
89
|
+
test("handles readContract errors", async () => {
|
|
90
|
+
vi.mocked(mockClient.getContractSchema).mockResolvedValue({ methods: { getData: { readonly: true } } });
|
|
91
|
+
vi.mocked(mockClient.readContract).mockRejectedValue(new Error("Mocked read error"));
|
|
92
|
+
|
|
93
|
+
await callActions.call({ contractAddress: "0xMockedContract", method: "getData", args: [1] });
|
|
94
|
+
|
|
95
|
+
expect(callActions["failSpinner"]).toHaveBeenCalledWith("Error during read operation", expect.any(Error));
|
|
124
96
|
});
|
|
125
97
|
|
|
126
|
-
test("handles errors
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
new Error("Mocked write error")
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
await expect(
|
|
137
|
-
caller.call({
|
|
138
|
-
contractAddress: "0xMockedContract",
|
|
139
|
-
method: "updateData",
|
|
140
|
-
...options,
|
|
141
|
-
})
|
|
142
|
-
).rejects.toThrowError("Mocked write error");
|
|
143
|
-
|
|
144
|
-
expect(mockClient.writeContract).toHaveBeenCalled();
|
|
98
|
+
test("handles writeContract errors", async () => {
|
|
99
|
+
vi.mocked(mockClient.getContractSchema).mockResolvedValue({ methods: { updateData: { readonly: false } } });
|
|
100
|
+
vi.mocked(mockClient.writeContract).mockRejectedValue(new Error("Mocked write error"));
|
|
101
|
+
|
|
102
|
+
await callActions.call({ contractAddress: "0xMockedContract", method: "updateData", args: [1] });
|
|
103
|
+
|
|
104
|
+
expect(callActions["failSpinner"]).toHaveBeenCalledWith("Error during write operation", expect.any(Error));
|
|
145
105
|
});
|
|
146
|
-
});
|
|
106
|
+
});
|