diff --git a/hooks_toolkit/asyncio_set_hook.py b/hooks_toolkit/asyncio_set_hook.py new file mode 100644 index 0000000..c120e13 --- /dev/null +++ b/hooks_toolkit/asyncio_set_hook.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python +# coding: utf-8 + +from typing import List, Dict, Any + +from xrpl.clients.sync_client import SyncClient +from xrpl.wallet import Wallet +from xrpl.models.transactions import SetHook +from xrpl.models.transactions.set_hook import Hook +from xrpl.utils import calculate_hook_on +from xrpl.models.transactions import SetHookFlag + +from hooks_toolkit.libs.asyncio.xrpl_helpers.transaction import ( + get_transaction_fee, + app_transaction, +) +from hooks_toolkit.utils import hex_namespace, read_hook_binary_hex_from_ns +from hooks_toolkit.types import SetHookParams + + +def create_hook_payload( + params: SetHookParams, +) -> Hook: + kwargs: Dict[str, Any] = { + "hook_api_version": params.version, + "hook_namespace": hex_namespace(params.namespace), + } + + if params.create_file is not None: + kwargs["create_code"] = read_hook_binary_hex_from_ns(params.create_file) + + if params.hook_on_array is not None: + kwargs["hook_on"] = calculate_hook_on(params.hook_on_array) + + if params.hook_hash is not None: + kwargs["hook_hash"] = params.hook_hash + + if params.flags is not None: + kwargs["flags"] = params.flags + + if params.hook_parameters is not None: + kwargs["hook_parameters"] = params.hook_parameters + + if params.hook_grants is not None: + kwargs["hook_grants"] = params.hook_grants + + return Hook(**kwargs) + + +async def set_hooks_v3(client: SyncClient, seed: str, hooks: List[Hook]): + HOOK_ACCOUNT = Wallet(seed, 0) + _tx = SetHook( + account=HOOK_ACCOUNT.classic_address, + hooks=hooks, + ) + tx = SetHook( + account=HOOK_ACCOUNT.classic_address, + hooks=hooks, + fee=await get_transaction_fee(client, _tx), + ) + + await app_transaction( + client, tx, HOOK_ACCOUNT, hard_fail=True, count=2, delay_ms=1000 + ) + + +async def clear_all_hooks_v3(client: SyncClient, seed: str): + HOOK_ACCOUNT = Wallet(seed, 0) + hook = Hook( + **{ + "create_code": "", + "flags": [SetHookFlag.HSF_OVERRIDE, SetHookFlag.HSF_NS_DELETE], + } + ) + _tx = SetHook( + account=HOOK_ACCOUNT.classic_address, + hooks=[hook, hook, hook, hook, hook, hook, hook, hook, hook, hook], + ) + tx = SetHook( + account=HOOK_ACCOUNT.classic_address, + hooks=[hook, hook, hook, hook, hook, hook, hook, hook, hook, hook], + fee=await get_transaction_fee(client, _tx), + ) + + await app_transaction( + client, tx, HOOK_ACCOUNT, hard_fail=True, count=2, delay_ms=1000 + ) + + +async def clear_hook_state_v3(client: SyncClient, seed: str, hooks: List[Hook]): + HOOK_ACCOUNT = Wallet(seed, 0) + _tx = SetHook( + account=HOOK_ACCOUNT.classic_address, + hooks=hooks, + ) + tx = SetHook( + account=HOOK_ACCOUNT.classic_address, + hooks=hooks, + fee=await get_transaction_fee(client, _tx), + ) + await app_transaction( + client, tx, HOOK_ACCOUNT, hard_fail=True, count=2, delay_ms=1000 + ) diff --git a/hooks_toolkit/asyncio_xrpld.py b/hooks_toolkit/asyncio_xrpld.py new file mode 100644 index 0000000..9ddf8e2 --- /dev/null +++ b/hooks_toolkit/asyncio_xrpld.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# coding: utf-8 + +from typing import Any +from xrpl.clients import Client + +from hooks_toolkit.libs.asyncio.xrpl_helpers.transaction import app_transaction + +from hooks_toolkit.types import SmartContractParams +from hooks_toolkit.libs.keylet_utils import ExecutionUtility + + +class Xrpld: + @staticmethod + async def submit(client: Client, params: SmartContractParams) -> Any: + # validate(built_tx) + tx_response = await app_transaction( + client, + params.tx, + params.wallet, + hard_fail=True, + count=1, + delay_ms=1000, + ) + tx_result = tx_response.result.get("meta")["TransactionResult"] + if tx_result == "tecHOOK_REJECTED": + hook_executions = ExecutionUtility.get_hook_executions_from_meta( + client, + tx_response.result.get("meta"), + ) + raise ValueError(hook_executions.executions[0].HookReturnString) + return tx_response.result diff --git a/hooks_toolkit/libs/asyncio/keylet_utils/__init__.py b/hooks_toolkit/libs/asyncio/keylet_utils/__init__.py new file mode 100644 index 0000000..f56c979 --- /dev/null +++ b/hooks_toolkit/libs/asyncio/keylet_utils/__init__.py @@ -0,0 +1,11 @@ +from hooks_toolkit.libs.keylet_utils.execution_utility import ( + ExecutionUtility, +) +from hooks_toolkit.libs.keylet_utils.state_utility import ( + StateUtility, +) + +__all__ = [ + "ExecutionUtility", + "StateUtility", +] diff --git a/hooks_toolkit/libs/asyncio/keylet_utils/execution_utility.py b/hooks_toolkit/libs/asyncio/keylet_utils/execution_utility.py new file mode 100644 index 0000000..f74e07d --- /dev/null +++ b/hooks_toolkit/libs/asyncio/keylet_utils/execution_utility.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python +# coding: utf-8 + +from typing import List + +from xrpl.asyncio.clients import AsyncWebsocketClient +from xrpl.models import TransactionMetadata +from xrpl.models.transactions.metadata import ( + HookExecution, + HookExecutionFields, + HookEmission, + HookEmissionFields, +) +from xrpl.models.requests import Tx +from xrpl.utils.str_conversions import hex_to_str + + +class iHookExecution: + def __init__(self, hook_execution: HookExecutionFields): + self.HookAccount = hook_execution["HookAccount"] + self.HookEmitCount = hook_execution["HookEmitCount"] + self.HookExecutionIndex = hook_execution["HookExecutionIndex"] + self.HookHash = hook_execution["HookHash"] + self.HookInstructionCount = hook_execution["HookInstructionCount"] + self.HookResult = hook_execution["HookResult"] + self.HookReturnCode = hook_execution["HookReturnCode"] + self.HookReturnString = hex_to_str(hook_execution["HookReturnString"]).replace( + "\x00", "" + ) + self.HookStateChangeCount = hook_execution["HookStateChangeCount"] + self.Flags = hook_execution["Flags"] + + +class iHookEmission: + def __init__(self, hook_emission: HookEmissionFields): + self.EmittedTxnID = hook_emission["EmittedTxnID"] + self.HookAccount = hook_emission["HookAccount"] + self.HookHash = hook_emission["HookHash"] + self.EmitNonce = hook_emission["EmitNonce"] + + +class iHookExecutions: + def __init__(self, results: List[HookExecution]): + self.executions = [iHookExecution(entry["HookExecution"]) for entry in results] + + +class iHookEmissions: + def __init__(self, results: List[HookEmission]): + self.txs = [iHookEmission(entry["HookEmission"]) for entry in results] + + +class ExecutionUtility: + @staticmethod + async def get_hook_executions_from_meta(meta: TransactionMetadata): + if not meta["HookExecutions"]: + raise Exception("No HookExecutions found") + + return iHookExecutions(meta["HookExecutions"]) + + @staticmethod + async def get_hook_executions_from_tx(client: AsyncWebsocketClient, hash: str): + if not client.is_open(): + raise Exception("xrpl Client is not connected") + + tx_response = await client.request(Tx(transaction=hash)) + + hook_executions = tx_response.result.get("meta", {}).get("HookExecutions") + if not hook_executions: + raise Exception("No HookExecutions found") + + return iHookExecutions(hook_executions) + + @staticmethod + async def get_hook_emitted_txs_from_meta(meta: TransactionMetadata): + if not meta["HookEmissions"]: + raise Exception("No HookEmissions found") + + return iHookEmissions(meta["HookEmissions"]) diff --git a/hooks_toolkit/libs/asyncio/keylet_utils/state_utility.py b/hooks_toolkit/libs/asyncio/keylet_utils/state_utility.py new file mode 100644 index 0000000..99257af --- /dev/null +++ b/hooks_toolkit/libs/asyncio/keylet_utils/state_utility.py @@ -0,0 +1,57 @@ +from xrpl.asyncio.clients import AsyncWebsocketClient +from xrpl.models.requests import LedgerEntry +from xrpl.models.requests.ledger_entry import Hook, HookDefinition, HookState +from xrpl.models.requests.account_namespace import AccountNamespace + + +class StateUtility: + @staticmethod + async def get_hook(client: AsyncWebsocketClient, account: str) -> Hook: + if not client.is_open(): + raise Exception("xrpl Client is not connected") + hook_req = LedgerEntry( + hook={ + "account": account, + }, + ) + hook_res = await client.request(hook_req) + return hook_res.result["node"] + + @staticmethod + async def get_hook_definition( + client: AsyncWebsocketClient, hash: str + ) -> HookDefinition: + if not client.is_open(): + raise Exception("xrpl Client is not connected") + hook_def_request = LedgerEntry( + hook_definition=hash, + ) + hook_def_res = await client.request(hook_def_request) + return hook_def_res.result["node"] + + @staticmethod + async def get_hook_state_dir( + client: AsyncWebsocketClient, account: str, namespace: str + ): + if not client.is_open(): + raise Exception("xrpl Client is not connected") + response = await client.request( + AccountNamespace(account=account, namespace_id=namespace) + ) + return response.result["namespace_entries"] + + @staticmethod + async def get_hook_state( + client: AsyncWebsocketClient, account: str, key: str, namespace: str + ) -> HookState: + if not client.is_open(): + raise Exception("xrpl Client is not connected") + hook_state_req = LedgerEntry( + hook_state={ + "account": account, + "key": key, + "namespace_id": namespace, + }, + ) + hook_state_resp = await client.request(hook_state_req) + return hook_state_resp.result["node"] diff --git a/hooks_toolkit/libs/asyncio/xrpl_helpers/__init__.py b/hooks_toolkit/libs/asyncio/xrpl_helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hooks_toolkit/libs/asyncio/xrpl_helpers/fund_system.py b/hooks_toolkit/libs/asyncio/xrpl_helpers/fund_system.py new file mode 100644 index 0000000..c4c26f5 --- /dev/null +++ b/hooks_toolkit/libs/asyncio/xrpl_helpers/fund_system.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# coding: utf-8 + +from typing import List + +from xrpl.asyncio.clients import Client +from xrpl.wallet import Wallet +from xrpl.models.amounts import IssuedCurrencyAmount + +from hooks_toolkit.libs.asyncio.xrpl_helpers.tools import ( + Account, + ICXRP, + balance, + fund, + account_set, + limit, + trust, + pay, +) + + +async def fund_system( + client: Client, + wallet: Wallet, + ic: IssuedCurrencyAmount, + native_balance: int, + ic_limit: int, + ic_balance: int, +) -> None: + user_accounts = [ + "alice", + "bob", + "carol", + "dave", + "elsa", + "frank", + "grace", + "heidi", + "ivan", + "judy", + ] + user_wallets: List[Account] = [Account(acct) for acct in user_accounts] + hook_accounts = [ + "hook1", + "hook2", + "hook3", + "hook4", + "hook5", + ] + hook_wallets: List[Account] = [Account(hacct) for hacct in hook_accounts] + + USD = ic + + gw: Account = Account("gw") + if await balance(client, gw.wallet.classic_address) == 0: + await fund(client, wallet, ICXRP(native_balance), gw.wallet.classic_address) + await account_set(client, gw.wallet) + + needs_funding = [] + needs_lines = [] + needs_ic = [] + + for acct in user_wallets: + if await balance(client, acct.wallet.classic_address) < (native_balance / 2): + needs_funding.append(acct.wallet.classic_address) + if await limit(client, acct.wallet.classic_address, USD) < (ic_limit / 2): + needs_lines.append(acct.wallet) + if await balance(client, acct.wallet.classic_address, USD) < (ic_balance / 2): + needs_ic.append(acct.wallet.classic_address) + + for hacct in hook_wallets: + if await balance(client, hacct.wallet.classic_address) < (native_balance / 2): + needs_funding.append(hacct.wallet.classic_address) + + print(f"FUNDING: {len(needs_funding)}") + print(f"TRUSTING: {len(needs_lines)}") + print(f"PAYING: {len(needs_ic)}") + + await fund(client, wallet, ICXRP(native_balance), *needs_funding) + await trust(client, USD.set(ic_limit), *needs_lines) + await pay(client, USD.set(ic_balance), gw.wallet, *needs_ic) diff --git a/hooks_toolkit/libs/asyncio/xrpl_helpers/setup.py b/hooks_toolkit/libs/asyncio/xrpl_helpers/setup.py new file mode 100644 index 0000000..96892d0 --- /dev/null +++ b/hooks_toolkit/libs/asyncio/xrpl_helpers/setup.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python +# coding: utf-8 + +import os +from xrpl.asyncio.clients import AsyncWebsocketClient +from xrpl.wallet import Wallet +from xrpl.asyncio.ledger import get_network_id + +from hooks_toolkit.libs.xrpl_helpers.constants import ( + NOT_ACTIVE_WALLET, + MASTER_ACCOUNT_WALLET, + GW_ACCOUNT_WALLET, + ALICE_ACCOUNT_WALLET, + BOB_ACCOUNT_WALLET, + CAROL_ACCOUNT_WALLET, + DAVE_ACCOUNT_WALLET, + ELSA_ACCOUNT_WALLET, + FRANK_ACCOUNT_WALLET, + GRACE_ACCOUNT_WALLET, + HEIDI_ACCOUNT_WALLET, + IVAN_ACCOUNT_WALLET, + JUDY_ACCOUNT_WALLET, + HOOK1_ACCOUNT_WALLET, + HOOK2_ACCOUNT_WALLET, + HOOK3_ACCOUNT_WALLET, + HOOK4_ACCOUNT_WALLET, + HOOK5_ACCOUNT_WALLET, +) +from hooks_toolkit.libs.asyncio.xrpl_helpers.fund_system import fund_system +from hooks_toolkit.libs.xrpl_helpers.tools import IC + + +class XrplIntegrationTestContext: + def __init__( + self, + client: AsyncWebsocketClient, + notactive: Wallet, + master: Wallet, + gw: Wallet, + ic: IC, + alice: Wallet, + bob: Wallet, + carol: Wallet, + dave: Wallet, + elsa: Wallet, + frank: Wallet, + grace: Wallet, + heidi: Wallet, + ivan: Wallet, + judy: Wallet, + hook1: Wallet, + hook2: Wallet, + hook3: Wallet, + hook4: Wallet, + hook5: Wallet, + ): + self.client = client + self.notactive = notactive + self.master = master + self.gw = gw + self.ic = ic + self.alice = alice + self.bob = bob + self.carol = carol + self.dave = dave + self.elsa = elsa + self.frank = frank + self.grace = grace + self.heidi = heidi + self.ivan = ivan + self.judy = judy + self.hook1 = hook1 + self.hook2 = hook2 + self.hook3 = hook3 + self.hook4 = hook4 + self.hook5 = hook5 + + +async def teardown_client(context: XrplIntegrationTestContext) -> None: + if not context or not context.client: + return + return await context.client.close() + + +async def setup_client( + server: str, + native_amount: int = 20000, + ic_limit: int = 100000, + ic_amount: int = 50000, +) -> XrplIntegrationTestContext: + currency = "USD" + async with AsyncWebsocketClient(server) as client: + RIPPLED_ENV = os.environ.get("RIPPLED_ENV", "standalone") + MASTER_NETWORK_WALLET: Wallet = MASTER_ACCOUNT_WALLET + if RIPPLED_ENV == "testnet" or RIPPLED_ENV == "mainnet": + # MASTER_NETWORK_WALLET: Wallet = generate_faucet_wallet( + # client, + # MASTER_NETWORK_WALLET, + # False, + # "https://xahau-test.net/accounts", + # ) + MASTER_NETWORK_WALLET: Wallet = Wallet("snSMyFp9vzqD2trMLhtETtdXVGsG8", 0) + native_amount: int = 200 + + context = XrplIntegrationTestContext( + client=client, + notactive=NOT_ACTIVE_WALLET, + master=MASTER_NETWORK_WALLET, + gw=GW_ACCOUNT_WALLET, + ic=IC.gw(currency, GW_ACCOUNT_WALLET.classic_address), + alice=ALICE_ACCOUNT_WALLET, + bob=BOB_ACCOUNT_WALLET, + carol=CAROL_ACCOUNT_WALLET, + dave=DAVE_ACCOUNT_WALLET, + elsa=ELSA_ACCOUNT_WALLET, + frank=FRANK_ACCOUNT_WALLET, + grace=GRACE_ACCOUNT_WALLET, + heidi=HEIDI_ACCOUNT_WALLET, + ivan=IVAN_ACCOUNT_WALLET, + judy=JUDY_ACCOUNT_WALLET, + hook1=HOOK1_ACCOUNT_WALLET, + hook2=HOOK2_ACCOUNT_WALLET, + hook3=HOOK3_ACCOUNT_WALLET, + hook4=HOOK4_ACCOUNT_WALLET, + hook5=HOOK5_ACCOUNT_WALLET, + ) + context.client.network_id = await get_network_id(client) + await fund_system( + context.client, + context.master, + context.ic, + native_amount, + ic_limit, + ic_amount, + ) + return context diff --git a/hooks_toolkit/libs/asyncio/xrpl_helpers/tools.py b/hooks_toolkit/libs/asyncio/xrpl_helpers/tools.py new file mode 100644 index 0000000..3945145 --- /dev/null +++ b/hooks_toolkit/libs/asyncio/xrpl_helpers/tools.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python +# coding: utf-8 + +from typing import Union, Dict, Any + +from xrpl.asyncio.clients import AsyncWebsocketClient +from xrpl.wallet import Wallet +from xrpl.models.amounts import IssuedCurrencyAmount +from xrpl.models.requests import AccountInfo, LedgerEntry +from xrpl.models.transactions import Payment, TrustSet, AccountSet, AccountSetFlag +from xrpl.utils import str_to_hex, xrp_to_drops + +from hooks_toolkit.libs.asyncio.xrpl_helpers.transaction import app_transaction +from hooks_toolkit.libs.xrpl_helpers.constants import ( + NOT_ACTIVE_WALLET, + GW_ACCOUNT_WALLET, + ALICE_ACCOUNT_WALLET, + BOB_ACCOUNT_WALLET, + CAROL_ACCOUNT_WALLET, + DAVE_ACCOUNT_WALLET, + ELSA_ACCOUNT_WALLET, + FRANK_ACCOUNT_WALLET, + GRACE_ACCOUNT_WALLET, + HEIDI_ACCOUNT_WALLET, + IVAN_ACCOUNT_WALLET, + JUDY_ACCOUNT_WALLET, + HOOK1_ACCOUNT_WALLET, + HOOK2_ACCOUNT_WALLET, + HOOK3_ACCOUNT_WALLET, + HOOK4_ACCOUNT_WALLET, + HOOK5_ACCOUNT_WALLET, +) + +LEDGER_ACCEPT_REQUEST = {"command": "ledger_accept"} + + +class Account: + def __init__(self, name: str = None, seed: str = None): + self.name = name + if name == "gw": + self.wallet = GW_ACCOUNT_WALLET + self.account = self.wallet.classic_address + if name == "notactivated": + self.wallet = NOT_ACTIVE_WALLET + self.account = self.wallet.classic_address + if name == "alice": + self.wallet = ALICE_ACCOUNT_WALLET + self.account = self.wallet.classic_address + if name == "bob": + self.wallet = BOB_ACCOUNT_WALLET + self.account = self.wallet.classic_address + if name == "carol": + self.wallet = CAROL_ACCOUNT_WALLET + self.account = self.wallet.classic_address + if name == "dave": + self.wallet = DAVE_ACCOUNT_WALLET + self.account = self.wallet.classic_address + if name == "elsa": + self.wallet = ELSA_ACCOUNT_WALLET + self.account = self.wallet.classic_address + if name == "frank": + self.wallet = FRANK_ACCOUNT_WALLET + self.account = self.wallet.classic_address + if name == "grace": + self.wallet = GRACE_ACCOUNT_WALLET + self.account = self.wallet.classic_address + if name == "heidi": + self.wallet = HEIDI_ACCOUNT_WALLET + self.account = self.wallet.classic_address + if name == "ivan": + self.wallet = IVAN_ACCOUNT_WALLET + self.account = self.wallet.classic_address + if name == "judy": + self.wallet = JUDY_ACCOUNT_WALLET + self.account = self.wallet.classic_address + if name == "hook1": + self.wallet = HOOK1_ACCOUNT_WALLET + self.account = self.wallet.classic_address + if name == "hook2": + self.wallet = HOOK2_ACCOUNT_WALLET + self.account = self.wallet.classic_address + if name == "hook3": + self.wallet = HOOK3_ACCOUNT_WALLET + self.account = self.wallet.classic_address + if name == "hook4": + self.wallet = HOOK4_ACCOUNT_WALLET + self.account = self.wallet.classic_address + if name == "hook5": + self.wallet = HOOK5_ACCOUNT_WALLET + self.account = self.wallet.classic_address + + +class ICXRP: + def __init__(self, value: int): + self.issuer = None + self.currency = "XRP" + self.value = value + self.amount = xrp_to_drops(self.value) + + +class IC: + def __init__(self, issuer: str, currency: str, value: int): + self.issuer = issuer + self.currency = currency + self.value = value + self.amount = IssuedCurrencyAmount( + currency=self.currency, value=str(self.value), issuer=self.issuer + ) + + @staticmethod + def gw(name: str, gw: str) -> "IC": + return IC(gw, name, 0) + + def set(self, value: int): + self.value = value + self.amount = IssuedCurrencyAmount( + currency=self.currency, value=str(self.value), issuer=self.issuer + ) + return self + + +async def account_seq(ctx: AsyncWebsocketClient, account: str) -> int: + request = AccountInfo(account=account) + try: + response = await ctx.request(request) + return response.result["account_data"]["Sequence"] + except Exception as error: + print(error) + return 0 + + +async def xrp_balance(ctx: AsyncWebsocketClient, account: str) -> float: + request = AccountInfo(account=account) + response = await ctx.request(request) + if "error" in response.result and response.result["error"] == "actNotFound": + return 0 + return float(response.result["account_data"]["Balance"]) + + +async def ic_balance(ctx: AsyncWebsocketClient, account: str, ic: IC) -> float: + request = LedgerEntry( + ripple_state={ + "currency": ic.currency, + "accounts": [account, ic.issuer], + } + ) + response = await ctx.request(request) + if "error" in response.result: + return 0 + node = response.result["node"] + return abs(float(node["Balance"]["value"])) + + +async def balance( + ctx: AsyncWebsocketClient, account: str, ic: Union[IC, None] = None +) -> float: + try: + if not ic: + return await xrp_balance(ctx, account) + return await ic_balance(ctx, account, ic) + except Exception as error: + print(error) + return 0 + + +async def limit(ctx: AsyncWebsocketClient, account: str, ic: IC) -> float: + try: + request = LedgerEntry( + ripple_state={ + "currency": ic.currency, + "accounts": [account, ic.issuer], + } + ) + response = await ctx.request(request) + if "error" in response.result: + return 0 + node = response.result["node"] + if node["HighLimit"]["issuer"] == ic.issuer: + return float(node["LowLimit"]["value"]) + else: + return float(node["HighLimit"]["value"]) + except Exception as error: + print(error) + return 0 + + +async def fund( + ctx: AsyncWebsocketClient, wallet: Wallet, uicx: Union[IC, ICXRP], *accts: str +) -> None: + for acct in accts: + try: + built_tx = Payment( + account=wallet.classic_address, + destination=acct, + amount=uicx.amount, + ) + await app_transaction( + ctx, + built_tx, + wallet, + ) + except Exception as error: + print(error) + # print(error.data.decoded) + # print(error.data.tx) + + +async def pay( + ctx: AsyncWebsocketClient, uicx: Union[IC, ICXRP], signer: Wallet, *accts: str +) -> None: + for acct in accts: + try: + built_tx = Payment( + account=signer.classic_address, + destination=acct, + amount=uicx.amount, + ) + await app_transaction( + ctx, + built_tx, + signer, + ) + except Exception as error: + print(error) + + +async def trust( + ctx: AsyncWebsocketClient, uicx: Union[IC, ICXRP], *accts: Wallet +) -> None: + for acct in accts: + try: + built_tx = TrustSet( + account=acct.classic_address, + limit_amount=uicx.amount, + ) + await app_transaction( + ctx, + built_tx, + acct, + ) + except Exception as error: + print(error) + + +async def account_set(ctx: AsyncWebsocketClient, account: Wallet) -> None: + built_tx = AccountSet( + account=account.classic_address, + transfer_rate=0, + domain=str_to_hex("https://usd.transia.io"), + set_flag=AccountSetFlag.ASF_DEFAULT_RIPPLE, + ) + await app_transaction( + ctx, + built_tx, + account, + ) + + +async def rpc_tx( + ctx: AsyncWebsocketClient, account: Wallet, json: Dict[str, Any] +) -> None: + await app_transaction( + ctx, + json, + account, + ) + + +async def rpc(ctx: AsyncWebsocketClient, json: Dict[str, Any]) -> None: + await ctx.request(json) + + +async def close(ctx: AsyncWebsocketClient) -> None: + await ctx.request(LEDGER_ACCEPT_REQUEST) diff --git a/hooks_toolkit/libs/asyncio/xrpl_helpers/transaction.py b/hooks_toolkit/libs/asyncio/xrpl_helpers/transaction.py new file mode 100644 index 0000000..b329555 --- /dev/null +++ b/hooks_toolkit/libs/asyncio/xrpl_helpers/transaction.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# coding: utf-8 + +import json +import os +from typing import Union, List + +from xrpl.asyncio.transaction import ( + sign_and_submit, + autofill, + safe_sign_and_autofill_transaction, + send_reliable_submission, +) +from xrpl.asyncio.clients import AsyncWebsocketClient +from xrpl.models import GenericRequest, Response, Transaction +from xrpl.wallet import Wallet +from xrpl.core.binarycodec import encode +from xrpl.asyncio.ledger import get_fee_estimate +from xrpl.models.requests import Tx + +LEDGER_ACCEPT_REQUEST = GenericRequest(method="ledger_accept") + + +async def verify_submitted_transaction( + client: AsyncWebsocketClient, tx: Union[Transaction, str] +) -> Response: + # hash = hash_tx if tx else hash_signed_tx(tx) + hash = tx + data = await client.request(Tx(transaction=hash)) + + # assert data.result + # assert data.result == (decode(tx) if isinstance(tx, str) else tx) + # if isinstance(data.result.meta, dict): + # assert data.result.meta["TransactionResult"] == "tesSUCCESS" + # else: + # assert data.result.meta == "tesSUCCESS" + return data + + +async def get_transaction_fee(client: AsyncWebsocketClient, transaction: Transaction): + # copy_tx = transaction.to_xrpl() + # copy_tx["Fee"] = "0" + # copy_tx["SigningPubKey"] = "" + prepared_tx = await autofill(transaction, client) + + tx_blob = encode(prepared_tx.to_xrpl()) + + result = await get_fee_estimate(client, tx_blob) + + return result + + +envs: List[str] = ["production", "testnet", "mainnet"] + + +async def app_transaction( + client: AsyncWebsocketClient, + transaction: Transaction, + wallet: Wallet, + hard_fail: bool = True, + count: int = 0, + delay_ms: int = 0, +) -> Response: + if os.environ.get("RIPPLED_ENV") == "standalone": + return await test_transaction( + client, transaction, wallet, hard_fail, count, delay_ms + ) + + if os.environ.get("RIPPLED_ENV") in envs: + tx: Transaction = safe_sign_and_autofill_transaction( + transaction, wallet, client, check_fee=True + ) + return await send_reliable_submission(tx, client) + + raise ValueError("unimplemented") + + +async def test_transaction( + client: AsyncWebsocketClient, + transaction: Transaction, + wallet: Wallet, + hard_fail: bool, + count: int, + delay_ms: int, +) -> Response: + await client.request(LEDGER_ACCEPT_REQUEST) + + response = await sign_and_submit(transaction, wallet, client, True, False) + + assert response.type == "response" + + if response.result["engine_result"] != "tesSUCCESS": + print( + ( + f"Transaction was not successful. " + f"Expected response.result.engine_result to be tesSUCCESS " + f"but got {response.result['engine_result']}" + ) + ) + print("The transaction was: ", transaction) + print("The response was: ", json.dumps(response.result)) + + if hard_fail: + assert response.result["engine_result"] == "tesSUCCESS", response.result[ + "engine_result_message" + ] + + await client.request(LEDGER_ACCEPT_REQUEST) + return await verify_submitted_transaction( + client, response.result["tx_json"]["hash"] + ) diff --git a/hooks_toolkit/libs/binary_models/utils/decode.py b/hooks_toolkit/libs/binary_models/utils/decode.py index ab0d7a2..e9b8a11 100644 --- a/hooks_toolkit/libs/binary_models/utils/decode.py +++ b/hooks_toolkit/libs/binary_models/utils/decode.py @@ -25,8 +25,6 @@ def decode_field( field_value: Any, field_type: str, max_string_length: int = None ) -> str: - print(field_type) - print(field_type == "uint8") if field_type == "uint8": return hex_to_uint8(field_value) elif field_type == "uint32": diff --git a/tests/integration/asyncio_integration_test_case.py b/tests/integration/asyncio_integration_test_case.py new file mode 100644 index 0000000..b9b828c --- /dev/null +++ b/tests/integration/asyncio_integration_test_case.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# coding: utf-8 + +import asyncio +from unittest import IsolatedAsyncioTestCase + +from hooks_toolkit.libs.xrpl_helpers.server_url import server_url +from hooks_toolkit.libs.xrpl_helpers.setup import ( + XrplIntegrationTestContext, +) +from hooks_toolkit.libs.asyncio.xrpl_helpers.setup import setup_client, teardown_client + + +class AsyncioIntegrationTestCase(IsolatedAsyncioTestCase): + context: XrplIntegrationTestContext + + @classmethod + async def asyncSetUpClass(cls): + cls.context = await setup_client(server_url) + + @classmethod + async def asyncTearDownClass(cls): + await teardown_client(cls.context) + + @classmethod + def setUpClass(cls): + asyncio.run(cls.asyncSetUpClass()) + + @classmethod + def tearDownClass(cls): + asyncio.run(cls.asyncTearDownClass()) diff --git a/tests/integration/libs/asyncio/keylet_utils/test_execution_utility.py b/tests/integration/libs/asyncio/keylet_utils/test_execution_utility.py new file mode 100644 index 0000000..fd095f5 --- /dev/null +++ b/tests/integration/libs/asyncio/keylet_utils/test_execution_utility.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python +# coding: utf-8 + +from tests.integration.asyncio_integration_test_case import AsyncioIntegrationTestCase +from hooks_toolkit.set_hook import create_hook_payload +from xrpl.models.transactions import SetHookFlag, Invoke +from xrpl.models.transactions.set_hook import Hook + +from hooks_toolkit.asyncio_xrpld import Xrpld +from hooks_toolkit.asyncio_set_hook import set_hooks_v3, clear_hook_state_v3 +from hooks_toolkit.types import SmartContractParams, SetHookParams +from hooks_toolkit.libs.asyncio.keylet_utils.execution_utility import ExecutionUtility + + +class TestExecutionUtility(AsyncioIntegrationTestCase): + async def test_executions_from_meta(cls): + async with cls.context.client as _: + hook = create_hook_payload( + SetHookParams( + version=0, + create_file="state_basic", + namespace="state_basic", + flags=[SetHookFlag.HSF_OVERRIDE], + hook_on_array=["Invoke"], + ) + ) + + await set_hooks_v3(cls.context.client, cls.context.hook1.seed, [hook]) + + hook_wallet = cls.context.hook1 + built_tx = Invoke( + account=hook_wallet.classic_address, + ) + + response = await Xrpld.submit( + cls.context.client, SmartContractParams(hook_wallet, built_tx) + ) + executions = await ExecutionUtility.get_hook_executions_from_meta( + response["meta"] + ) + cls.assertEqual( + executions.executions[0].HookReturnCode, + "22", + ) + cls.assertEqual( + executions.executions[0].HookHash, + "B1F39E63D27603F1A2E7E804E92514FAC721F353D849B0787288F5026809AD84", + ) + + # clear hook + hook = Hook(**{"create_code": "", "flags": [SetHookFlag.HSF_OVERRIDE]}) + await set_hooks_v3(cls.context.client, cls.context.hook1.seed, [hook]) + clear_hook = create_hook_payload( + SetHookParams( + flags=[SetHookFlag.HSF_NS_DELETE], + namespace="state_basic", + ) + ) + await clear_hook_state_v3( + cls.context.client, cls.context.hook1.seed, [clear_hook] + ) + + async def test_executions_from_tx_hash(cls): + async with cls.context.client as _: + hook = create_hook_payload( + SetHookParams( + version=0, + create_file="state_basic", + namespace="state_basic", + flags=[SetHookFlag.HSF_OVERRIDE], + hook_on_array=["Invoke"], + ) + ) + + await set_hooks_v3(cls.context.client, cls.context.hook1.seed, [hook]) + + hook_wallet = cls.context.hook1 + built_tx = Invoke( + account=hook_wallet.classic_address, + ) + + response = await Xrpld.submit( + cls.context.client, SmartContractParams(hook_wallet, built_tx) + ) + executions = await ExecutionUtility.get_hook_executions_from_tx( + cls.context.client, response["hash"] + ) + cls.assertEqual( + executions.executions[0].HookReturnCode, + "22", + ) + cls.assertEqual( + executions.executions[0].HookHash, + "B1F39E63D27603F1A2E7E804E92514FAC721F353D849B0787288F5026809AD84", + ) + + # clear hook + hook = Hook(**{"create_code": "", "flags": [SetHookFlag.HSF_OVERRIDE]}) + await set_hooks_v3(cls.context.client, cls.context.hook1.seed, [hook]) + clear_hook = create_hook_payload( + SetHookParams( + flags=[SetHookFlag.HSF_NS_DELETE], + namespace="state_basic", + ) + ) + await clear_hook_state_v3( + cls.context.client, cls.context.hook1.seed, [clear_hook] + ) + + async def test_emissions_from_meta(cls): + async with cls.context.client as _: + hook = create_hook_payload( + SetHookParams( + version=0, + create_file="callback", + namespace="callback", + flags=[SetHookFlag.HSF_OVERRIDE], + hook_on_array=["Invoke"], + ) + ) + + await set_hooks_v3(cls.context.client, cls.context.hook1.seed, [hook]) + + hook_wallet = cls.context.hook1 + built_tx = Invoke( + account=hook_wallet.classic_address, + ) + + response = await Xrpld.submit( + cls.context.client, SmartContractParams(hook_wallet, built_tx) + ) + emissions = await ExecutionUtility.get_hook_emitted_txs_from_meta( + response["meta"] + ) + cls.assertEqual( + emissions.txs[0].HookHash, + "2E079E2D4D5C54386612F323C3BF0689942E8856CCE23DD262793C20A15D0A0B", + ) + cls.assertEqual( + emissions.txs[0].HookAccount, + "rBpVrkKc8QnxsCGsngMJgmDKqxJKoWHfKt", + ) + + # clear hook + hook = Hook(**{"create_code": "", "flags": [SetHookFlag.HSF_OVERRIDE]}) + await set_hooks_v3(cls.context.client, cls.context.hook1.seed, [hook]) + clear_hook = create_hook_payload( + SetHookParams( + flags=[SetHookFlag.HSF_NS_DELETE], + namespace="state_basic", + ) + ) + await clear_hook_state_v3( + cls.context.client, cls.context.hook1.seed, [clear_hook] + ) diff --git a/tests/integration/libs/asyncio/keylet_utils/test_state_utility.py b/tests/integration/libs/asyncio/keylet_utils/test_state_utility.py new file mode 100644 index 0000000..0cd4f3b --- /dev/null +++ b/tests/integration/libs/asyncio/keylet_utils/test_state_utility.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python +# coding: utf-8 + +from tests.integration.asyncio_integration_test_case import AsyncioIntegrationTestCase +from hooks_toolkit.set_hook import create_hook_payload +from xrpl.models.transactions import SetHookFlag, Invoke +from xrpl.models.transactions.set_hook import Hook + +from hooks_toolkit.asyncio_xrpld import Xrpld +from hooks_toolkit.asyncio_set_hook import set_hooks_v3, clear_hook_state_v3 +from hooks_toolkit.types import SmartContractParams, SetHookParams +from hooks_toolkit.libs.asyncio.keylet_utils.state_utility import StateUtility +from hooks_toolkit.utils import hex_namespace, pad_hex_string, flip_hex +from hooks_toolkit.libs.binary_models import xrp_address_to_hex, hex_to_uint64 + + +class TestStateUtility(AsyncioIntegrationTestCase): + async def test_get_hook(cls): + async with cls.context.client as _: + hook = create_hook_payload( + SetHookParams( + version=0, + create_file="state_basic", + namespace="state_basic", + flags=[SetHookFlag.HSF_OVERRIDE], + hook_on_array=["Invoke"], + ) + ) + + await set_hooks_v3(cls.context.client, cls.context.hook1.seed, [hook]) + + hook_wallet = cls.context.hook1 + built_tx = Invoke( + account=hook_wallet.classic_address, + ) + + await Xrpld.submit( + cls.context.client, SmartContractParams(hook_wallet, built_tx) + ) + + hook = await StateUtility.get_hook( + cls.context.client, + hook_wallet.classic_address, + ) + cls.assertEqual( + hook["Hooks"][0]["Hook"]["HookHash"], + "B1F39E63D27603F1A2E7E804E92514FAC721F353D849B0787288F5026809AD84", + ) + + # clear hook + hook = Hook(**{"create_code": "", "flags": [SetHookFlag.HSF_OVERRIDE]}) + await set_hooks_v3(cls.context.client, cls.context.hook1.seed, [hook]) + clear_hook = create_hook_payload( + SetHookParams( + flags=[SetHookFlag.HSF_NS_DELETE], + namespace="state_basic", + ) + ) + await clear_hook_state_v3( + cls.context.client, cls.context.hook1.seed, [clear_hook] + ) + + async def test_get_hook_definition(cls): + async with cls.context.client as _: + hook = create_hook_payload( + SetHookParams( + version=0, + create_file="state_basic", + namespace="state_basic", + flags=[SetHookFlag.HSF_OVERRIDE], + hook_on_array=["Invoke"], + ) + ) + + await set_hooks_v3(cls.context.client, cls.context.hook1.seed, [hook]) + + hook_wallet = cls.context.hook1 + built_tx = Invoke( + account=hook_wallet.classic_address, + ) + + await Xrpld.submit( + cls.context.client, SmartContractParams(hook_wallet, built_tx) + ) + + hook_definition = await StateUtility.get_hook_definition( + cls.context.client, + "B1F39E63D27603F1A2E7E804E92514FAC721F353D849B0787288F5026809AD84", + ) + cls.assertEqual( + hook_definition["LedgerEntryType"], + "HookDefinition", + ) + + # clear hook + hook = Hook(**{"create_code": "", "flags": [SetHookFlag.HSF_OVERRIDE]}) + await set_hooks_v3(cls.context.client, cls.context.hook1.seed, [hook]) + clear_hook = create_hook_payload( + SetHookParams( + flags=[SetHookFlag.HSF_NS_DELETE], + namespace="state_basic", + ) + ) + await clear_hook_state_v3( + cls.context.client, cls.context.hook1.seed, [clear_hook] + ) + + async def test_hook_state_dir(cls): + async with cls.context.client as _: + hook = create_hook_payload( + SetHookParams( + version=0, + create_file="state_basic", + namespace="state_basic", + flags=[SetHookFlag.HSF_OVERRIDE], + hook_on_array=["Invoke"], + ) + ) + + await set_hooks_v3(cls.context.client, cls.context.hook1.seed, [hook]) + + hook_wallet = cls.context.hook1 + built_tx = Invoke( + account=hook_wallet.classic_address, + ) + + await Xrpld.submit( + cls.context.client, SmartContractParams(hook_wallet, built_tx) + ) + + hook_state_dir = await StateUtility.get_hook_state_dir( + cls.context.client, + hook_wallet.classic_address, + hex_namespace("state_basic"), + ) + cls.assertEqual( + len(hook_state_dir), + 1, + ) + # clear hook + hook = Hook(**{"create_code": "", "flags": [SetHookFlag.HSF_OVERRIDE]}) + await set_hooks_v3(cls.context.client, cls.context.hook1.seed, [hook]) + clear_hook = create_hook_payload( + SetHookParams( + flags=[SetHookFlag.HSF_NS_DELETE], + namespace="state_basic", + ) + ) + await clear_hook_state_v3( + cls.context.client, cls.context.hook1.seed, [clear_hook] + ) + + async def test_hook_state(cls): + async with cls.context.client as _: + hook = create_hook_payload( + SetHookParams( + version=0, + create_file="state_basic", + namespace="state_basic", + flags=[SetHookFlag.HSF_OVERRIDE], + hook_on_array=["Invoke"], + ) + ) + + await set_hooks_v3(cls.context.client, cls.context.hook1.seed, [hook]) + + hook_wallet = cls.context.hook1 + built_tx = Invoke( + account=hook_wallet.classic_address, + ) + + await Xrpld.submit( + cls.context.client, SmartContractParams(hook_wallet, built_tx) + ) + + hook_state = await StateUtility.get_hook_state( + cls.context.client, + hook_wallet.classic_address, + pad_hex_string(xrp_address_to_hex(hook_wallet.classic_address)), + hex_namespace("state_basic"), + ) + cls.assertGreater( + hex_to_uint64(flip_hex(hook_state["HookStateData"])), + 0, + ) + # clear hook + hook = Hook(**{"create_code": "", "flags": [SetHookFlag.HSF_OVERRIDE]}) + await set_hooks_v3(cls.context.client, cls.context.hook1.seed, [hook]) + clear_hook = create_hook_payload( + SetHookParams( + flags=[SetHookFlag.HSF_NS_DELETE], + namespace="state_basic", + ) + ) + await clear_hook_state_v3( + cls.context.client, cls.context.hook1.seed, [clear_hook] + )