Skip to content

Commit 6f5235b

Browse files
tests: almanac registrations
1 parent 6ad556b commit 6f5235b

File tree

3 files changed

+260
-1
lines changed

3 files changed

+260
-1
lines changed

tests/e2e/entities/test_almanac.py

+222
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import json
2+
import sys
3+
import time
4+
import unittest
5+
from dataclasses import dataclass
6+
from pathlib import Path
7+
from typing import List, Dict, Tuple
8+
9+
import graphql
10+
from cosmpy.aerial.tx_helpers import SubmittedTx
11+
from gql import gql
12+
13+
repo_root_path = Path(__file__).parent.parent.parent.parent.absolute()
14+
sys.path.insert(0, str(repo_root_path))
15+
sys.path.insert(0, str(Path(repo_root_path, "uagents")))
16+
17+
from uagents.src.nexus.crypto import Identity
18+
19+
from src.genesis.helpers.field_enums import AlmanacRegistrations, AlmanacRecords, Agents
20+
from tests.helpers.contracts import AlmanacContract, DefaultAlmanacContractConfig
21+
from tests.helpers.entity_test import EntityTest
22+
from tests.helpers.graphql import test_filtered_query
23+
from tests.helpers.regexes import msg_id_regex, tx_id_regex, block_id_regex
24+
25+
26+
def gql_by_expiry_height(registration_node: Dict) -> int:
27+
return int(registration_node["expiryHeight"])
28+
29+
30+
def sql_by_expiry_height(registration_row: Tuple) -> int:
31+
return int(registration_row[AlmanacRegistrations.expiry_height.value])
32+
33+
34+
@dataclass
35+
class Scenario:
36+
name: str
37+
query: graphql.DocumentNode
38+
expected: any
39+
40+
41+
class TestAlmanac(EntityTest):
42+
test_registrations_endpoints = [
43+
"127.0.0.1:9999",
44+
"127.0.0.1:8888",
45+
"127.0.0.1:7777",
46+
"127.0.0.1:6666"
47+
]
48+
submitted_txs: List[SubmittedTx] = []
49+
expected_registrations: List[Dict] = []
50+
expected_records: List[Dict] = [
51+
{
52+
"service": {
53+
"protocols": ["grpc"],
54+
"endpoints": [{
55+
"url": endpoint,
56+
# NB: not "proper" usage of weight; for testing only
57+
"weight": i
58+
}]
59+
}
60+
} for (i, endpoint) in enumerate(test_registrations_endpoints)
61+
]
62+
63+
@classmethod
64+
def setUpClass(cls):
65+
super().setUpClass()
66+
cls.clean_db({"almanac_registrations", "almanac_resolutions"})
67+
cls._contract = AlmanacContract(cls.ledger_client, cls.validator_wallet)
68+
69+
# NB: broadcast multiple registrations
70+
for (i, expected_record) in enumerate(cls.expected_records):
71+
# Create agent identity
72+
identity = Identity.from_seed("alice recovery password", i)
73+
agent_address = str(identity.address)
74+
75+
# Get sequence
76+
query_msg = {"query_sequence": {"agent_address": agent_address}}
77+
sequence = cls._contract.query(query_msg)["sequence"]
78+
79+
signature = identity.sign_registration(
80+
contract_address=str(cls._contract.address),
81+
sequence=sequence)
82+
83+
tx = cls._contract.execute({
84+
"register": {
85+
"agent_address": agent_address,
86+
"record": expected_record,
87+
"sequence": sequence,
88+
"signature": signature,
89+
}
90+
}, cls.validator_wallet, funds=DefaultAlmanacContractConfig.register_stake_funds)
91+
tx.wait_to_complete()
92+
cls.submitted_txs.append(tx)
93+
cls.expected_registrations.append({
94+
"agentId": agent_address,
95+
"expiryHeight": tx.response.height + DefaultAlmanacContractConfig.expiry_height,
96+
"sequence": sequence,
97+
"signature": signature,
98+
"record": expected_record
99+
})
100+
# NB: wait for the indexer
101+
time.sleep(7)
102+
103+
def test_registrations_sql(self):
104+
registrations = self.db_cursor.execute(AlmanacRegistrations.select_query()).fetchall()
105+
actual_reg_length = len(registrations)
106+
107+
expected_registrations_count = len(self.expected_registrations)
108+
self.assertEqual(expected_registrations_count,
109+
actual_reg_length,
110+
f"expected {expected_registrations_count} registrations; got {actual_reg_length}")
111+
# NB: sort by expiry height so that indexes match
112+
# their respective scenario.expected index
113+
list.sort(registrations, key=sql_by_expiry_height)
114+
for (i, registration) in enumerate(registrations):
115+
self.assertEqual(self.expected_registrations[i]["agentId"], registration[AlmanacRegistrations.agent_id.value])
116+
self.assertLess(self.submitted_txs[i].response.height,
117+
registration[AlmanacRegistrations.expiry_height.value])
118+
self.assertRegex(registration[AlmanacRegistrations.id.value], msg_id_regex)
119+
self.assertRegex(registration[AlmanacRegistrations.transaction_id.value], tx_id_regex)
120+
self.assertRegex(registration[AlmanacRegistrations.block_id.value], block_id_regex)
121+
122+
def matches_expected_record(_record: Dict) -> bool:
123+
return _record["service"]["endpoints"][0]["weight"] == i
124+
125+
# Lookup related record
126+
record = self.db_cursor.execute(
127+
AlmanacRecords.select_where(
128+
f"almanac_records.id = '{registration[AlmanacRegistrations.record_id.value]}'",
129+
[AlmanacRecords.table, AlmanacRegistrations.table])).fetchone()
130+
expected_record = next(r for r in self.expected_records if matches_expected_record(r))
131+
self.assertIsNotNone(record)
132+
self.assertIsNotNone(expected_record)
133+
self.assertDictEqual(expected_record["service"], record[AlmanacRecords.service.value])
134+
self.assertRegex(record[AlmanacRecords.id.value], msg_id_regex)
135+
self.assertRegex(record[AlmanacRecords.transaction_id.value], tx_id_regex)
136+
self.assertRegex(record[AlmanacRecords.block_id.value], block_id_regex)
137+
138+
# Lookup related agent
139+
agent = self.db_cursor.execute(Agents.select_where(
140+
f"id = '{registration[AlmanacRegistrations.agent_id.value]}'"
141+
)).fetchone()
142+
self.assertIsNotNone(agent)
143+
144+
def test_registrations_gql(self):
145+
registrations_nodes = """
146+
{
147+
id
148+
expiryHeight
149+
agentId
150+
contractId
151+
record {
152+
id
153+
service
154+
# registrationId
155+
# eventId
156+
transactionId
157+
blockId
158+
}
159+
transactionId
160+
blockId
161+
}
162+
"""
163+
164+
last_tx_height = self.submitted_txs[-1].response.height
165+
expired_registrations_query = test_filtered_query("almanacRegistrations", {
166+
"expiryHeight": {
167+
"lessThanOrEqualTo": str(last_tx_height)
168+
}
169+
}, registrations_nodes)
170+
171+
active_registrations_query = test_filtered_query("almanacRegistrations", {
172+
"expiryHeight": {
173+
"greaterThan": str(last_tx_height)
174+
}
175+
}, registrations_nodes)
176+
177+
all_registrations_query = gql("query {almanacRegistrations {nodes " + registrations_nodes + "}}")
178+
179+
last_expired_height = last_tx_height - DefaultAlmanacContractConfig.expiry_height
180+
last_expired = next(r for r in self.submitted_txs if r.response.height == last_expired_height)
181+
last_expired_index = self.submitted_txs.index(last_expired)
182+
scenarios = [
183+
Scenario(
184+
name="expired registrations",
185+
query=expired_registrations_query,
186+
expected=self.expected_registrations[0: last_expired_index + 1]
187+
),
188+
Scenario(
189+
name="active registrations",
190+
query=active_registrations_query,
191+
expected=self.expected_registrations[last_expired_index + 1:]
192+
),
193+
Scenario(
194+
name="all registrations",
195+
query=all_registrations_query,
196+
expected=self.expected_registrations
197+
),
198+
]
199+
200+
for scenario in scenarios:
201+
with self.subTest(scenario.name):
202+
gql_result = self.gql_client.execute(scenario.query)
203+
registrations = gql_result["almanacRegistrations"]["nodes"]
204+
self.assertEqual(len(scenario.expected), len(registrations))
205+
206+
# TODO: use respective gql order by when available
207+
# NB: sort by expiry height so that indexes match
208+
# their respective scenario.expected index
209+
list.sort(registrations, key=gql_by_expiry_height)
210+
self.assertEqual(len(scenario.expected), len(registrations))
211+
212+
for (i, registration) in enumerate(registrations):
213+
self.assertRegex(registration["id"], msg_id_regex)
214+
self.assertEqual(scenario.expected[i]["agentId"], registration["agentId"])
215+
self.assertEqual(str(self._contract.address), registration["contractId"])
216+
self.assertEqual(str(scenario.expected[i]["expiryHeight"]), registration["expiryHeight"])
217+
self.assertRegex(registration["transactionId"], tx_id_regex)
218+
self.assertRegex(registration["blockId"], block_id_regex)
219+
# TODO: assert record equality
220+
221+
if __name__ == "__main__":
222+
unittest.main()

tests/helpers/contracts.py

+36
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
from dataclasses import dataclass
3+
from typing import Optional, Union
34

45
import requests
56
from cosmpy.aerial.client import LedgerClient
@@ -23,6 +24,21 @@ class BridgeContractConfig:
2324
next_swap_id: int
2425

2526

27+
@dataclass_json
28+
@dataclass
29+
class AlmanacContractConfig:
30+
stake_denom: str
31+
expiry_height: Optional[int]
32+
register_stake_amount: Optional[str]
33+
admin: Optional[str]
34+
35+
@property
36+
def register_stake_funds(self) -> Union[None, str]:
37+
if self.register_stake_amount == "0":
38+
return None
39+
return self.register_stake_amount + self.stake_denom
40+
41+
2642
DefaultBridgeContractConfig = BridgeContractConfig(
2743
cap="250000000000000000000000000",
2844
reverse_aggregated_allowance="3000000000000000000000000",
@@ -35,6 +51,13 @@ class BridgeContractConfig:
3551
next_swap_id=0,
3652
)
3753

54+
DefaultAlmanacContractConfig = AlmanacContractConfig(
55+
stake_denom="atestfet",
56+
expiry_height=2,
57+
register_stake_amount="0",
58+
admin=None
59+
)
60+
3861

3962
def ensure_contract(name: str, url: str) -> str:
4063
contract_path = f".contract/{name}.wasm"
@@ -122,3 +145,16 @@ def __init__(self, client: LedgerClient, admin: Wallet, cfg: BridgeContractConfi
122145
# and it will instantiate the contract only if contract.address is None
123146
# see: https://github.com/fetchai/cosmpy/blob/master/cosmpy/aerial/contract/__init__.py#L168-L179
124147
self.deploy(cfg.to_dict(), admin, store_gas_limit=3000000)
148+
149+
150+
class AlmanacContract(LedgerContract):
151+
def __init__(self, client: LedgerClient, admin: Wallet, cfg: AlmanacContractConfig = DefaultAlmanacContractConfig):
152+
url = "https://github.com/fetchai/contract-agent-almanac/releases/download/v0.1.1/contract_agent_almanac.wasm"
153+
contract_path = ensure_contract("almanac", url)
154+
super().__init__(contract_path, client)
155+
156+
self.deploy(
157+
cfg.to_dict(),
158+
admin,
159+
store_gas_limit=3000000
160+
)

tests/helpers/graphql.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import re
33
from typing import Dict
44

5+
import graphql
56
from gql import gql
67

78
json_keys_regex = re.compile('"(\w+)":')
@@ -12,7 +13,7 @@ def to_gql(obj: Dict):
1213
return json_keys_regex.sub("\g<1>:", json.dumps(obj))
1314

1415

15-
def test_filtered_query(root_entity: str, _filter: Dict, nodes_string: str, _order: str = ""):
16+
def test_filtered_query(root_entity: str, _filter: Dict, nodes_string: str, _order: str = "") -> graphql.DocumentNode:
1617
filter_string = to_gql(_filter)
1718

1819
return gql(

0 commit comments

Comments
 (0)