From 6bea24a4af7c155ed8b52d7afbc27d4c0e9d2d7b Mon Sep 17 00:00:00 2001 From: Lucas Willems Date: Thu, 13 Apr 2023 20:30:09 +0200 Subject: [PATCH 1/2] WIP --- contracts/pair_contract_gen.py | 154 ++++ gen_pair_contract.py | 101 +++ pair.abi.json | 1158 ++++++++++++++++++++++++++ tmp.py | 12 + utils/__init__.py | 0 utils/contract_interactor.py | 25 + utils/{utils_decode.py => decode.py} | 82 +- 7 files changed, 1510 insertions(+), 22 deletions(-) create mode 100644 contracts/pair_contract_gen.py create mode 100644 gen_pair_contract.py create mode 100644 pair.abi.json create mode 100644 tmp.py create mode 100644 utils/__init__.py create mode 100644 utils/contract_interactor.py rename utils/{utils_decode.py => decode.py} (62%) diff --git a/contracts/pair_contract_gen.py b/contracts/pair_contract_gen.py new file mode 100644 index 0000000..08713e4 --- /dev/null +++ b/contracts/pair_contract_gen.py @@ -0,0 +1,154 @@ +from multiversx_sdk_core import Address + +from utils.contract_interactor import ContractInteractor +from utils.decode import d + + +class PairContractInteractor(ContractInteractor): + def getTokensForGivenPosition(self, liquidity: int): + return self._query("getTokensForGivenPosition", [liquidity], [EsdtTokenPayment_decoder, EsdtTokenPayment_decoder]) + + def getReservesAndTotalSupply(self): + return self._query("getReservesAndTotalSupply", [], [d.U(), d.U(), d.U()]) + + def getAmountOut(self, token_in: str, amount_in: int): + return self._query("getAmountOut", [token_in, amount_in], [d.U()]) + + def getAmountIn(self, token_wanted: str, amount_wanted: int): + return self._query("getAmountIn", [token_wanted, amount_wanted], [d.U()]) + + def getEquivalent(self, token_in: str, amount_in: int): + return self._query("getEquivalent", [token_in, amount_in], [d.U()]) + + def getFeeState(self): + return self._query("getFeeState", [], [d.Bool()]) + + def getFeeDestinations(self): + return self._query("getFeeDestinations", [], [d.Tuple([d.Addr(), d.Str()])]) + + def getTrustedSwapPairs(self): + return self._query("getTrustedSwapPairs", [], [d.Tuple([TokenPair_decoder, d.Addr()])]) + + def getWhitelistedManagedAddresses(self): + return self._query("getWhitelistedManagedAddresses", [], [d.Addr()]) + + def getFeesCollectorAddress(self): + return self._query("getFeesCollectorAddress", [], [d.Addr()]) + + def getFeesCollectorCutPercentage(self): + return self._query("getFeesCollectorCutPercentage", [], [d.U64()]) + + def getLpTokenIdentifier(self): + return self._query("getLpTokenIdentifier", [], [d.Str()]) + + def getTotalFeePercent(self): + return self._query("getTotalFeePercent", [], [d.U64()]) + + def getSpecialFee(self): + return self._query("getSpecialFee", [], [d.U64()]) + + def getRouterManagedAddress(self): + return self._query("getRouterManagedAddress", [], [d.Addr()]) + + def getFirstTokenId(self): + return self._query("getFirstTokenId", [], [d.Str()]) + + def getSecondTokenId(self): + return self._query("getSecondTokenId", [], [d.Str()]) + + def getTotalSupply(self): + return self._query("getTotalSupply", [], [d.U()]) + + def getInitialLiquidtyAdder(self): + return self._query("getInitialLiquidtyAdder", [], [d.Option(d.Addr())]) + + def getReserve(self, token_id: str): + return self._query("getReserve", [token_id], [d.U()]) + + def getLockingScAddress(self): + return self._query("getLockingScAddress", [], [d.Addr()]) + + def getUnlockEpoch(self): + return self._query("getUnlockEpoch", [], [d.U64()]) + + def getLockingDeadlineEpoch(self): + return self._query("getLockingDeadlineEpoch", [], [d.U64()]) + + def getPermissions(self, address: Address): + return self._query("getPermissions", [address], [d.U32()]) + + def getState(self): + return self._query("getState", [], [State_decoder]) + + +AddLiquidityEvent_decoder = d.Tuple({ + "caller": d.Addr(), + "first_token_id": d.Str(), + "first_token_amount": d.U(), + "second_token_id": d.Str(), + "second_token_amount": d.U(), + "lp_token_id": d.Str(), + "lp_token_amount": d.U(), + "lp_supply": d.U(), + "first_token_reserves": d.U(), + "second_token_reserves": d.U(), + "block": d.U64(), + "epoch": d.U64(), + "timestamp": d.U64(), +}) + +EsdtTokenPayment_decoder = d.Tuple({ + "token_identifier": d.Str(), + "token_nonce": d.U64(), + "amount": d.U(), +}) + +RemoveLiquidityEvent_decoder = d.Tuple({ + "caller": d.Addr(), + "first_token_id": d.Str(), + "first_token_amount": d.U(), + "second_token_id": d.Str(), + "second_token_amount": d.U(), + "lp_token_id": d.Str(), + "lp_token_amount": d.U(), + "lp_supply": d.U(), + "first_token_reserves": d.U(), + "second_token_reserves": d.U(), + "block": d.U64(), + "epoch": d.U64(), + "timestamp": d.U64(), +}) + +State_decoder = d.U8() + +SwapEvent_decoder = d.Tuple({ + "caller": d.Addr(), + "token_id_in": d.Str(), + "token_amount_in": d.U(), + "token_id_out": d.Str(), + "token_amount_out": d.U(), + "fee_amount": d.U(), + "token_in_reserve": d.U(), + "token_out_reserve": d.U(), + "block": d.U64(), + "epoch": d.U64(), + "timestamp": d.U64(), +}) + +SwapNoFeeAndForwardEvent_decoder = d.Tuple({ + "caller": d.Addr(), + "token_id_in": d.Str(), + "token_amount_in": d.U(), + "token_id_out": d.Str(), + "token_amount_out": d.U(), + "destination": d.Addr(), + "block": d.U64(), + "epoch": d.U64(), + "timestamp": d.U64(), +}) + +TokenPair_decoder = d.Tuple({ + "first_token": d.Str(), + "second_token": d.Str(), +}) + diff --git a/gen_pair_contract.py b/gen_pair_contract.py new file mode 100644 index 0000000..b4d4032 --- /dev/null +++ b/gen_pair_contract.py @@ -0,0 +1,101 @@ +import json +import re +import sys + + +# Example of command: +# `python3 gen_pair_contract.py pair.abi.json contracts/pair_contract_gen.py` + + +def abi_type_to_decoder_code(type: str): + match = re.search(r"variadic<(.+)>", type) + if match: + return abi_type_to_decoder_code(match.group(1)) + match = re.search(r"Option<(.+)>", type) + if match: + inside_dec_code = abi_type_to_decoder_code(match.group(1)) + return f"d.Option({inside_dec_code})" + match = re.search(r"tuple<(.+)>", type) + if match: + codes = [abi_type_to_decoder_code(t) for t in match.group(1).split(",")] + code = ", ".join(codes) + return f"d.Tuple([{code}])" + if type == "bool": + return "d.Bool()" + if type == "u8": + return "d.U8()" + if type == "u16": + return "d.U16()" + if type == "u32": + return "d.U32()" + if type == "u64": + return "d.U64()" + if type == "BigUint": + return "d.U()" + if type == "Address": + return "d.Addr()" + if type == "TokenIdentifier": + return "d.Str()" + return f"{type}_decoder" + + +if len(sys.argv) < 3: + raise ValueError("2 arguments must be provided: path to ABI and path to generated script.") + +with open(sys.argv[1], "r") as file: + data = json.load(file) + +code = """from multiversx_sdk_core import Address + +from utils.contract_interactor import ContractInteractor +from utils.decode import d + + +""" + +contract_name = data["name"] + +code += f"class {contract_name}ContractInteractor(ContractInteractor):\n" + +for e_info in data["endpoints"]: + if e_info["mutability"] != "readonly": + continue + e_name = e_info["name"] + inputs_code = "" + arg_codes = [] + for i_info in e_info["inputs"]: + inputs_code += ", " + i_info["name"] + i_type = i_info["type"] + if i_type == "TokenIdentifier": + inputs_code += ": str" + elif i_type == "Address": + inputs_code += ": Address" + elif i_type == "BigUint": + inputs_code += ": int" + arg_codes.append(i_info["name"]) + args_code = ", ".join(arg_codes) + decoder_codes = [] + for o_info in e_info["outputs"]: + decoder_codes.append(abi_type_to_decoder_code(o_info["type"])) + decoders_code = ", ".join(decoder_codes) + code += f"\tdef {e_name}(self{inputs_code}):\n\t\treturn self._query(\"{e_name}\", [{args_code}], [{decoders_code}])\n\n" + +code += "\n" + +for t_name, t_info in data["types"].items(): + if t_info["type"] == "struct": + code += f"{t_name}_decoder = d.Tuple({{\n" + for f_info in t_info["fields"]: + f_name = f_info["name"] + f_type = f_info["type"] + code += f"\t\"{f_name}\": " + code += abi_type_to_decoder_code(f_type) + code += ",\n" + code += "})\n\n" + elif t_info["type"] == "enum": + code += f"{t_name}_decoder = d.U8()\n\n" + else: + raise ValueError("Unknown type.") + +with open(sys.argv[2], "w") as file: + file.write(code) diff --git a/pair.abi.json b/pair.abi.json new file mode 100644 index 0000000..be002e5 --- /dev/null +++ b/pair.abi.json @@ -0,0 +1,1158 @@ +{ + "buildInfo": { + "rustc": { + "version": "1.70.0-nightly", + "commitHash": "22f247c6f3ed388cb702d01c2ff27da658a8b353", + "commitDate": "2023-03-13", + "channel": "Nightly", + "short": "rustc 1.70.0-nightly (22f247c6f 2023-03-13)" + }, + "contractCrate": { + "name": "pair", + "version": "0.0.0", + "gitVersion": "v1.6.0-1266-g30817f6c" + }, + "framework": { + "name": "multiversx-sc", + "version": "0.39.4" + } + }, + "name": "Pair", + "constructor": { + "inputs": [ + { + "name": "first_token_id", + "type": "TokenIdentifier" + }, + { + "name": "second_token_id", + "type": "TokenIdentifier" + }, + { + "name": "router_address", + "type": "Address" + }, + { + "name": "router_owner_address", + "type": "Address" + }, + { + "name": "total_fee_percent", + "type": "u64" + }, + { + "name": "special_fee_percent", + "type": "u64" + }, + { + "name": "initial_liquidity_adder", + "type": "Address" + }, + { + "name": "admins", + "type": "variadic
", + "multi_arg": true + } + ], + "outputs": [] + }, + "endpoints": [ + { + "name": "addInitialLiquidity", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [], + "outputs": [ + { + "type": "EsdtTokenPayment" + }, + { + "type": "EsdtTokenPayment" + }, + { + "type": "EsdtTokenPayment" + } + ] + }, + { + "name": "addLiquidity", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "first_token_amount_min", + "type": "BigUint" + }, + { + "name": "second_token_amount_min", + "type": "BigUint" + } + ], + "outputs": [ + { + "type": "EsdtTokenPayment" + }, + { + "type": "EsdtTokenPayment" + }, + { + "type": "EsdtTokenPayment" + } + ] + }, + { + "name": "removeLiquidity", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "first_token_amount_min", + "type": "BigUint" + }, + { + "name": "second_token_amount_min", + "type": "BigUint" + } + ], + "outputs": [ + { + "type": "EsdtTokenPayment" + }, + { + "type": "EsdtTokenPayment" + } + ] + }, + { + "name": "removeLiquidityAndBuyBackAndBurnToken", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "token_to_buyback_and_burn", + "type": "TokenIdentifier" + } + ], + "outputs": [] + }, + { + "name": "swapNoFeeAndForward", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "token_out", + "type": "TokenIdentifier" + }, + { + "name": "destination_address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "swapTokensFixedInput", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "token_out", + "type": "TokenIdentifier" + }, + { + "name": "amount_out_min", + "type": "BigUint" + } + ], + "outputs": [ + { + "type": "EsdtTokenPayment" + } + ] + }, + { + "name": "swapTokensFixedOutput", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [ + { + "name": "token_out", + "type": "TokenIdentifier" + }, + { + "name": "amount_out", + "type": "BigUint" + } + ], + "outputs": [ + { + "type": "EsdtTokenPayment" + }, + { + "type": "EsdtTokenPayment" + } + ] + }, + { + "name": "setLpTokenIdentifier", + "mutability": "mutable", + "inputs": [ + { + "name": "token_identifier", + "type": "TokenIdentifier" + } + ], + "outputs": [] + }, + { + "name": "getTokensForGivenPosition", + "mutability": "readonly", + "inputs": [ + { + "name": "liquidity", + "type": "BigUint" + } + ], + "outputs": [ + { + "type": "EsdtTokenPayment" + }, + { + "type": "EsdtTokenPayment" + } + ] + }, + { + "name": "getReservesAndTotalSupply", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "BigUint" + }, + { + "type": "BigUint" + }, + { + "type": "BigUint" + } + ] + }, + { + "name": "getAmountOut", + "mutability": "readonly", + "inputs": [ + { + "name": "token_in", + "type": "TokenIdentifier" + }, + { + "name": "amount_in", + "type": "BigUint" + } + ], + "outputs": [ + { + "type": "BigUint" + } + ] + }, + { + "name": "getAmountIn", + "mutability": "readonly", + "inputs": [ + { + "name": "token_wanted", + "type": "TokenIdentifier" + }, + { + "name": "amount_wanted", + "type": "BigUint" + } + ], + "outputs": [ + { + "type": "BigUint" + } + ] + }, + { + "name": "getEquivalent", + "mutability": "readonly", + "inputs": [ + { + "name": "token_in", + "type": "TokenIdentifier" + }, + { + "name": "amount_in", + "type": "BigUint" + } + ], + "outputs": [ + { + "type": "BigUint" + } + ] + }, + { + "name": "getFeeState", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "bool" + } + ] + }, + { + "name": "whitelist", + "mutability": "mutable", + "inputs": [ + { + "name": "address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "removeWhitelist", + "mutability": "mutable", + "inputs": [ + { + "name": "address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "addTrustedSwapPair", + "mutability": "mutable", + "inputs": [ + { + "name": "pair_address", + "type": "Address" + }, + { + "name": "first_token", + "type": "TokenIdentifier" + }, + { + "name": "second_token", + "type": "TokenIdentifier" + } + ], + "outputs": [] + }, + { + "name": "removeTrustedSwapPair", + "mutability": "mutable", + "inputs": [ + { + "name": "first_token", + "type": "TokenIdentifier" + }, + { + "name": "second_token", + "type": "TokenIdentifier" + } + ], + "outputs": [] + }, + { + "docs": [ + "`fees_collector_cut_percentage` of the special fees are sent to the fees_collector_address SC", + "", + "For example, if special fees is 5%, and fees_collector_cut_percentage is 10%,", + "then of the 5%, 10% are reserved, and only the rest are split between other pair contracts." + ], + "name": "setupFeesCollector", + "mutability": "mutable", + "inputs": [ + { + "name": "fees_collector_address", + "type": "Address" + }, + { + "name": "fees_collector_cut_percentage", + "type": "u64" + } + ], + "outputs": [] + }, + { + "name": "setFeeOn", + "mutability": "mutable", + "inputs": [ + { + "name": "enabled", + "type": "bool" + }, + { + "name": "fee_to_address", + "type": "Address" + }, + { + "name": "fee_token", + "type": "TokenIdentifier" + } + ], + "outputs": [] + }, + { + "name": "getFeeDestinations", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "variadic>", + "multi_result": true + } + ] + }, + { + "name": "getTrustedSwapPairs", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "variadic>", + "multi_result": true + } + ] + }, + { + "name": "getWhitelistedManagedAddresses", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "variadic
", + "multi_result": true + } + ] + }, + { + "name": "getFeesCollectorAddress", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "Address" + } + ] + }, + { + "name": "getFeesCollectorCutPercentage", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u64" + } + ] + }, + { + "name": "setStateActiveNoSwaps", + "mutability": "mutable", + "inputs": [], + "outputs": [] + }, + { + "name": "setFeePercents", + "mutability": "mutable", + "inputs": [ + { + "name": "total_fee_percent", + "type": "u64" + }, + { + "name": "special_fee_percent", + "type": "u64" + } + ], + "outputs": [] + }, + { + "name": "getLpTokenIdentifier", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "TokenIdentifier" + } + ] + }, + { + "name": "getTotalFeePercent", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u64" + } + ] + }, + { + "name": "getSpecialFee", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u64" + } + ] + }, + { + "name": "getRouterManagedAddress", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "Address" + } + ] + }, + { + "name": "getFirstTokenId", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "TokenIdentifier" + } + ] + }, + { + "name": "getSecondTokenId", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "TokenIdentifier" + } + ] + }, + { + "name": "getTotalSupply", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "BigUint" + } + ] + }, + { + "name": "getInitialLiquidtyAdder", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "Option
" + } + ] + }, + { + "name": "getReserve", + "mutability": "readonly", + "inputs": [ + { + "name": "token_id", + "type": "TokenIdentifier" + } + ], + "outputs": [ + { + "type": "BigUint" + } + ] + }, + { + "name": "updateAndGetTokensForGivenPositionWithSafePrice", + "mutability": "mutable", + "inputs": [ + { + "name": "liquidity", + "type": "BigUint" + } + ], + "outputs": [ + { + "type": "EsdtTokenPayment" + }, + { + "type": "EsdtTokenPayment" + } + ] + }, + { + "name": "updateAndGetSafePrice", + "mutability": "mutable", + "inputs": [ + { + "name": "input", + "type": "EsdtTokenPayment" + } + ], + "outputs": [ + { + "type": "EsdtTokenPayment" + } + ] + }, + { + "name": "setMaxObservationsPerRecord", + "mutability": "mutable", + "inputs": [ + { + "name": "max_observations_per_record", + "type": "u64" + } + ], + "outputs": [] + }, + { + "name": "setLockingDeadlineEpoch", + "mutability": "mutable", + "inputs": [ + { + "name": "new_deadline", + "type": "u64" + } + ], + "outputs": [] + }, + { + "name": "setLockingScAddress", + "mutability": "mutable", + "inputs": [ + { + "name": "new_address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "setUnlockEpoch", + "mutability": "mutable", + "inputs": [ + { + "name": "new_epoch", + "type": "u64" + } + ], + "outputs": [] + }, + { + "name": "getLockingScAddress", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "Address" + } + ] + }, + { + "name": "getUnlockEpoch", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u64" + } + ] + }, + { + "name": "getLockingDeadlineEpoch", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u64" + } + ] + }, + { + "name": "addAdmin", + "mutability": "mutable", + "inputs": [ + { + "name": "address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "removeAdmin", + "mutability": "mutable", + "inputs": [ + { + "name": "address", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "updateOwnerOrAdmin", + "onlyOwner": true, + "mutability": "mutable", + "inputs": [ + { + "name": "previous_owner", + "type": "Address" + } + ], + "outputs": [] + }, + { + "name": "getPermissions", + "mutability": "readonly", + "inputs": [ + { + "name": "address", + "type": "Address" + } + ], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "name": "addToPauseWhitelist", + "mutability": "mutable", + "inputs": [ + { + "name": "address_list", + "type": "variadic
", + "multi_arg": true + } + ], + "outputs": [] + }, + { + "name": "removeFromPauseWhitelist", + "mutability": "mutable", + "inputs": [ + { + "name": "address_list", + "type": "variadic
", + "multi_arg": true + } + ], + "outputs": [] + }, + { + "name": "pause", + "mutability": "mutable", + "inputs": [], + "outputs": [] + }, + { + "name": "resume", + "mutability": "mutable", + "inputs": [], + "outputs": [] + }, + { + "name": "getState", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "State" + } + ] + } + ], + "events": [ + { + "identifier": "swap", + "inputs": [ + { + "name": "token_in", + "type": "TokenIdentifier", + "indexed": true + }, + { + "name": "token_out", + "type": "TokenIdentifier", + "indexed": true + }, + { + "name": "caller", + "type": "Address", + "indexed": true + }, + { + "name": "epoch", + "type": "u64", + "indexed": true + }, + { + "name": "swap_event", + "type": "SwapEvent" + } + ] + }, + { + "identifier": "swap_no_fee_and_forward", + "inputs": [ + { + "name": "token_id_out", + "type": "TokenIdentifier", + "indexed": true + }, + { + "name": "caller", + "type": "Address", + "indexed": true + }, + { + "name": "epoch", + "type": "u64", + "indexed": true + }, + { + "name": "swap_no_fee_and_forward_event", + "type": "SwapNoFeeAndForwardEvent" + } + ] + }, + { + "identifier": "add_liquidity", + "inputs": [ + { + "name": "first_token", + "type": "TokenIdentifier", + "indexed": true + }, + { + "name": "second_token", + "type": "TokenIdentifier", + "indexed": true + }, + { + "name": "caller", + "type": "Address", + "indexed": true + }, + { + "name": "epoch", + "type": "u64", + "indexed": true + }, + { + "name": "add_liquidity_event", + "type": "AddLiquidityEvent" + } + ] + }, + { + "identifier": "remove_liquidity", + "inputs": [ + { + "name": "first_token", + "type": "TokenIdentifier", + "indexed": true + }, + { + "name": "second_token", + "type": "TokenIdentifier", + "indexed": true + }, + { + "name": "caller", + "type": "Address", + "indexed": true + }, + { + "name": "epoch", + "type": "u64", + "indexed": true + }, + { + "name": "remove_liquidity_event", + "type": "RemoveLiquidityEvent" + } + ] + } + ], + "hasCallback": false, + "types": { + "AddLiquidityEvent": { + "type": "struct", + "fields": [ + { + "name": "caller", + "type": "Address" + }, + { + "name": "first_token_id", + "type": "TokenIdentifier" + }, + { + "name": "first_token_amount", + "type": "BigUint" + }, + { + "name": "second_token_id", + "type": "TokenIdentifier" + }, + { + "name": "second_token_amount", + "type": "BigUint" + }, + { + "name": "lp_token_id", + "type": "TokenIdentifier" + }, + { + "name": "lp_token_amount", + "type": "BigUint" + }, + { + "name": "lp_supply", + "type": "BigUint" + }, + { + "name": "first_token_reserves", + "type": "BigUint" + }, + { + "name": "second_token_reserves", + "type": "BigUint" + }, + { + "name": "block", + "type": "u64" + }, + { + "name": "epoch", + "type": "u64" + }, + { + "name": "timestamp", + "type": "u64" + } + ] + }, + "EsdtTokenPayment": { + "type": "struct", + "fields": [ + { + "name": "token_identifier", + "type": "TokenIdentifier" + }, + { + "name": "token_nonce", + "type": "u64" + }, + { + "name": "amount", + "type": "BigUint" + } + ] + }, + "RemoveLiquidityEvent": { + "type": "struct", + "fields": [ + { + "name": "caller", + "type": "Address" + }, + { + "name": "first_token_id", + "type": "TokenIdentifier" + }, + { + "name": "first_token_amount", + "type": "BigUint" + }, + { + "name": "second_token_id", + "type": "TokenIdentifier" + }, + { + "name": "second_token_amount", + "type": "BigUint" + }, + { + "name": "lp_token_id", + "type": "TokenIdentifier" + }, + { + "name": "lp_token_amount", + "type": "BigUint" + }, + { + "name": "lp_supply", + "type": "BigUint" + }, + { + "name": "first_token_reserves", + "type": "BigUint" + }, + { + "name": "second_token_reserves", + "type": "BigUint" + }, + { + "name": "block", + "type": "u64" + }, + { + "name": "epoch", + "type": "u64" + }, + { + "name": "timestamp", + "type": "u64" + } + ] + }, + "State": { + "type": "enum", + "variants": [ + { + "name": "Inactive", + "discriminant": 0 + }, + { + "name": "Active", + "discriminant": 1 + }, + { + "name": "PartialActive", + "discriminant": 2 + } + ] + }, + "SwapEvent": { + "type": "struct", + "fields": [ + { + "name": "caller", + "type": "Address" + }, + { + "name": "token_id_in", + "type": "TokenIdentifier" + }, + { + "name": "token_amount_in", + "type": "BigUint" + }, + { + "name": "token_id_out", + "type": "TokenIdentifier" + }, + { + "name": "token_amount_out", + "type": "BigUint" + }, + { + "name": "fee_amount", + "type": "BigUint" + }, + { + "name": "token_in_reserve", + "type": "BigUint" + }, + { + "name": "token_out_reserve", + "type": "BigUint" + }, + { + "name": "block", + "type": "u64" + }, + { + "name": "epoch", + "type": "u64" + }, + { + "name": "timestamp", + "type": "u64" + } + ] + }, + "SwapNoFeeAndForwardEvent": { + "type": "struct", + "fields": [ + { + "name": "caller", + "type": "Address" + }, + { + "name": "token_id_in", + "type": "TokenIdentifier" + }, + { + "name": "token_amount_in", + "type": "BigUint" + }, + { + "name": "token_id_out", + "type": "TokenIdentifier" + }, + { + "name": "token_amount_out", + "type": "BigUint" + }, + { + "name": "destination", + "type": "Address" + }, + { + "name": "block", + "type": "u64" + }, + { + "name": "epoch", + "type": "u64" + }, + { + "name": "timestamp", + "type": "u64" + } + ] + }, + "TokenPair": { + "type": "struct", + "fields": [ + { + "name": "first_token", + "type": "TokenIdentifier" + }, + { + "name": "second_token", + "type": "TokenIdentifier" + } + ] + } + } +} diff --git a/tmp.py b/tmp.py new file mode 100644 index 0000000..77479fd --- /dev/null +++ b/tmp.py @@ -0,0 +1,12 @@ +from multiversx_sdk_network_providers import ProxyNetworkProvider +from multiversx_sdk_core import Address + +from contracts.pair_contract_gen import PairContractInteractor + + +proxy = ProxyNetworkProvider("https://gateway.multiversx.com") +contract_address = Address.from_bech32("erd1qqqqqqqqqqqqqpgqp5d4x3d263x4alnapwafwujch5xqmvyq2jpsk2xhsy") +interactor = PairContractInteractor(proxy, contract_address) + +print(interactor.getState()) +print(interactor.getTokensForGivenPosition(1000)) diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/contract_interactor.py b/utils/contract_interactor.py new file mode 100644 index 0000000..bd467f8 --- /dev/null +++ b/utils/contract_interactor.py @@ -0,0 +1,25 @@ +from typing import Sequence +import base64 +from multiversx_sdk_network_providers import ProxyNetworkProvider +from multiversx_sdk_core import Address, ContractQueryBuilder + +from utils.decode import top_dec_bytes, Decoder + + +class ContractInteractor: + proxy: ProxyNetworkProvider + contract_address: Address + + def __init__(self, proxy: ProxyNetworkProvider, contract_address: Address): + self.proxy = proxy + self.contract_address = contract_address + + def _query(self, function: str, args: list, decoders: Sequence[Decoder]): + query = ContractQueryBuilder(self.contract_address, function, args).build() + b64_data = self.proxy.query_contract(query).return_data + dec_data = [] + for i in range(len(b64_data)): + b64 = b64_data[i] + decoder = decoders[min(i, len(decoders) - 1)] + dec_data.append(top_dec_bytes(base64.b64decode(b64), decoder)) + return dec_data diff --git a/utils/utils_decode.py b/utils/decode.py similarity index 62% rename from utils/utils_decode.py rename to utils/decode.py index bf3f9a3..629e904 100644 --- a/utils/utils_decode.py +++ b/utils/decode.py @@ -49,6 +49,14 @@ def nest_decode(self, b: ByteReader) -> int: return int.from_bytes(b.read(length), "big") +class AddressDecoder: + def top_decode(self, b: ByteReader) -> bytes: + return b.read(32) + + def nest_decode(self, b: ByteReader) -> bytes: + return self.top_decode(b) + + class StringDecoder: def top_decode(self, b: ByteReader) -> str: return b.read_all().decode() @@ -66,17 +74,50 @@ def nest_decode(self, b: ByteReader) -> bool: return self.top_decode(b) +class OptionDecoder: + decoder: Decoder + + def __init__(self, decoder: Decoder): + self.decoder = decoder + + def top_decode(self, b: ByteReader) -> Any: + if b.is_consumed(): + return None + flag = b.read(1) + if flag == b'\x01': + return self.decoder.nest_decode(b) + else: + raise ValueError("Invalid Option flag") + + def nest_decode(self, b: ByteReader) -> Any: + flag = b.read(1) + if flag == b'\x01': + return self.decoder.nest_decode(b) + elif flag == b'\x00': + return None + else: + raise ValueError("Invalid Option flag") + + class TupleDecoder: - decoders: Dict[str, Decoder] + decoders: List[Decoder] | Dict[str, Decoder] - def __init__(self, decoders: Dict[str, Decoder]): + def __init__(self, decoders: List[Decoder] | Dict[str, Decoder]): self.decoders = decoders - def top_decode(self, b: ByteReader) -> Dict[str, Any]: - result = {} - for key, decoder in self.decoders.items(): - result[key] = decoder.nest_decode(b) - return result + def top_decode(self, b: ByteReader) -> List[Any] | Dict[str, Any]: + if isinstance(self.decoders, list): + result = [] + for decoder in self.decoders: + result.append(decoder.nest_decode(b)) + return result + elif isinstance(self.decoders, dict): + result = {} + for key, decoder in self.decoders.items(): + result[key] = decoder.nest_decode(b) + return result + else: + raise ValueError("Invalid decoders.") def nest_decode(self, b: ByteReader) -> Dict[str, Any]: return self.top_decode(b) @@ -123,6 +164,10 @@ def U64(cls): def U(cls): return BigUintDecoder() + @classmethod + def Addr(cls): + return AddressDecoder() + @classmethod def Str(cls): return StringDecoder() @@ -131,6 +176,10 @@ def Str(cls): def Bool(cls): return BooleanDecoder() + @classmethod + def Option(cls, decoder: Decoder): + return OptionDecoder(decoder) + @classmethod def Tuple(cls, decoders: Dict[str, Decoder]): return TupleDecoder(decoders) @@ -140,19 +189,8 @@ def List(cls, decoder: Decoder): return ListDecoder(decoder) -def top_dec_hex(hex_str: bytes, decoder: Decoder): - return decoder.top_decode(ByteReader(bytes.fromhex(hex_str))) - +def top_dec_bytes(b: bytes, decoder: Decoder): + return decoder.top_decode(ByteReader(b)) -# decoded = top_dec_hex( -# "000000030c0d0e00000007626f6e6a6f7572000000010c", -# d.Tuple({ -# "a": d.List(d.U8()), -# "b": d.Tuple({ -# "a": d.Str(), -# "b": d.U(), -# }) -# }), -# ) -# print(decoded) -# # Output: {'a': [12, 13, 14], 'b': {'a': 'bonjour', 'b': 12}} +def top_dec_hex_str(hex_str: str, decoder: Decoder): + return top_dec_bytes(bytes.fromhex(hex_str), decoder) From 7f1c20775fc5ed74b9460c02facccabee1a53925 Mon Sep 17 00:00:00 2001 From: Lucas Willems Date: Fri, 14 Apr 2023 11:39:59 +0200 Subject: [PATCH 2/2] Add auto-generation of calls --- pair.abi.json => abis/pair.abi.json | 0 contracts/pair_contract_gen.py | 139 ++++++++++++++++++++++------ gen_pair_contract.py | 40 +++++--- tmp.py | 12 --- utils/contract_interactor.py | 57 ++++++++++-- 5 files changed, 189 insertions(+), 59 deletions(-) rename pair.abi.json => abis/pair.abi.json (100%) delete mode 100644 tmp.py diff --git a/pair.abi.json b/abis/pair.abi.json similarity index 100% rename from pair.abi.json rename to abis/pair.abi.json diff --git a/contracts/pair_contract_gen.py b/contracts/pair_contract_gen.py index 08713e4..e727c08 100644 --- a/contracts/pair_contract_gen.py +++ b/contracts/pair_contract_gen.py @@ -1,83 +1,170 @@ from multiversx_sdk_core import Address -from utils.contract_interactor import ContractInteractor +from utils.contract_interactor import ContractInteractor, Payment from utils.decode import d class PairContractInteractor(ContractInteractor): - def getTokensForGivenPosition(self, liquidity: int): + def call_addInitialLiquidity(self, _payment: Payment): + return self._call("addInitialLiquidity", [], 0, _payment) + + def call_addLiquidity(self, first_token_amount_min: int, second_token_amount_min: int, _payment: Payment): + return self._call("addLiquidity", [first_token_amount_min, second_token_amount_min], 0, _payment) + + def call_removeLiquidity(self, first_token_amount_min: int, second_token_amount_min: int, _payment: Payment): + return self._call("removeLiquidity", [first_token_amount_min, second_token_amount_min], 0, _payment) + + def call_removeLiquidityAndBuyBackAndBurnToken(self, token_to_buyback_and_burn: str, _payment: Payment): + return self._call("removeLiquidityAndBuyBackAndBurnToken", [token_to_buyback_and_burn], 0, _payment) + + def call_swapNoFeeAndForward(self, token_out: str, destination_address: Address, _payment: Payment): + return self._call("swapNoFeeAndForward", [token_out, destination_address], 0, _payment) + + def call_swapTokensFixedInput(self, token_out: str, amount_out_min: int, _payment: Payment): + return self._call("swapTokensFixedInput", [token_out, amount_out_min], 0, _payment) + + def call_swapTokensFixedOutput(self, token_out: str, amount_out: int, _payment: Payment): + return self._call("swapTokensFixedOutput", [token_out, amount_out], 0, _payment) + + def call_setLpTokenIdentifier(self, token_identifier: str): + return self._call("setLpTokenIdentifier", [token_identifier], 0) + + def query_getTokensForGivenPosition(self, liquidity: int): return self._query("getTokensForGivenPosition", [liquidity], [EsdtTokenPayment_decoder, EsdtTokenPayment_decoder]) - def getReservesAndTotalSupply(self): + def query_getReservesAndTotalSupply(self): return self._query("getReservesAndTotalSupply", [], [d.U(), d.U(), d.U()]) - def getAmountOut(self, token_in: str, amount_in: int): + def query_getAmountOut(self, token_in: str, amount_in: int): return self._query("getAmountOut", [token_in, amount_in], [d.U()]) - def getAmountIn(self, token_wanted: str, amount_wanted: int): + def query_getAmountIn(self, token_wanted: str, amount_wanted: int): return self._query("getAmountIn", [token_wanted, amount_wanted], [d.U()]) - def getEquivalent(self, token_in: str, amount_in: int): + def query_getEquivalent(self, token_in: str, amount_in: int): return self._query("getEquivalent", [token_in, amount_in], [d.U()]) - def getFeeState(self): + def query_getFeeState(self): return self._query("getFeeState", [], [d.Bool()]) - def getFeeDestinations(self): + def call_whitelist(self, address: Address): + return self._call("whitelist", [address], 0) + + def call_removeWhitelist(self, address: Address): + return self._call("removeWhitelist", [address], 0) + + def call_addTrustedSwapPair(self, pair_address: Address, first_token: str, second_token: str): + return self._call("addTrustedSwapPair", [pair_address, first_token, second_token], 0) + + def call_removeTrustedSwapPair(self, first_token: str, second_token: str): + return self._call("removeTrustedSwapPair", [first_token, second_token], 0) + + def call_setupFeesCollector(self, fees_collector_address: Address, fees_collector_cut_percentage): + return self._call("setupFeesCollector", [fees_collector_address, fees_collector_cut_percentage], 0) + + def call_setFeeOn(self, enabled, fee_to_address: Address, fee_token: str): + return self._call("setFeeOn", [enabled, fee_to_address, fee_token], 0) + + def query_getFeeDestinations(self): return self._query("getFeeDestinations", [], [d.Tuple([d.Addr(), d.Str()])]) - def getTrustedSwapPairs(self): + def query_getTrustedSwapPairs(self): return self._query("getTrustedSwapPairs", [], [d.Tuple([TokenPair_decoder, d.Addr()])]) - def getWhitelistedManagedAddresses(self): + def query_getWhitelistedManagedAddresses(self): return self._query("getWhitelistedManagedAddresses", [], [d.Addr()]) - def getFeesCollectorAddress(self): + def query_getFeesCollectorAddress(self): return self._query("getFeesCollectorAddress", [], [d.Addr()]) - def getFeesCollectorCutPercentage(self): + def query_getFeesCollectorCutPercentage(self): return self._query("getFeesCollectorCutPercentage", [], [d.U64()]) - def getLpTokenIdentifier(self): + def call_setStateActiveNoSwaps(self): + return self._call("setStateActiveNoSwaps", [], 0) + + def call_setFeePercents(self, total_fee_percent, special_fee_percent): + return self._call("setFeePercents", [total_fee_percent, special_fee_percent], 0) + + def query_getLpTokenIdentifier(self): return self._query("getLpTokenIdentifier", [], [d.Str()]) - def getTotalFeePercent(self): + def query_getTotalFeePercent(self): return self._query("getTotalFeePercent", [], [d.U64()]) - def getSpecialFee(self): + def query_getSpecialFee(self): return self._query("getSpecialFee", [], [d.U64()]) - def getRouterManagedAddress(self): + def query_getRouterManagedAddress(self): return self._query("getRouterManagedAddress", [], [d.Addr()]) - def getFirstTokenId(self): + def query_getFirstTokenId(self): return self._query("getFirstTokenId", [], [d.Str()]) - def getSecondTokenId(self): + def query_getSecondTokenId(self): return self._query("getSecondTokenId", [], [d.Str()]) - def getTotalSupply(self): + def query_getTotalSupply(self): return self._query("getTotalSupply", [], [d.U()]) - def getInitialLiquidtyAdder(self): + def query_getInitialLiquidtyAdder(self): return self._query("getInitialLiquidtyAdder", [], [d.Option(d.Addr())]) - def getReserve(self, token_id: str): + def query_getReserve(self, token_id: str): return self._query("getReserve", [token_id], [d.U()]) - def getLockingScAddress(self): + def call_updateAndGetTokensForGivenPositionWithSafePrice(self, liquidity: int): + return self._call("updateAndGetTokensForGivenPositionWithSafePrice", [liquidity], 0) + + def call_updateAndGetSafePrice(self, input): + return self._call("updateAndGetSafePrice", [input], 0) + + def call_setMaxObservationsPerRecord(self, max_observations_per_record): + return self._call("setMaxObservationsPerRecord", [max_observations_per_record], 0) + + def call_setLockingDeadlineEpoch(self, new_deadline): + return self._call("setLockingDeadlineEpoch", [new_deadline], 0) + + def call_setLockingScAddress(self, new_address: Address): + return self._call("setLockingScAddress", [new_address], 0) + + def call_setUnlockEpoch(self, new_epoch): + return self._call("setUnlockEpoch", [new_epoch], 0) + + def query_getLockingScAddress(self): return self._query("getLockingScAddress", [], [d.Addr()]) - def getUnlockEpoch(self): + def query_getUnlockEpoch(self): return self._query("getUnlockEpoch", [], [d.U64()]) - def getLockingDeadlineEpoch(self): + def query_getLockingDeadlineEpoch(self): return self._query("getLockingDeadlineEpoch", [], [d.U64()]) - def getPermissions(self, address: Address): + def call_addAdmin(self, address: Address): + return self._call("addAdmin", [address], 0) + + def call_removeAdmin(self, address: Address): + return self._call("removeAdmin", [address], 0) + + def call_updateOwnerOrAdmin(self, previous_owner: Address): + return self._call("updateOwnerOrAdmin", [previous_owner], 0) + + def query_getPermissions(self, address: Address): return self._query("getPermissions", [address], [d.U32()]) - def getState(self): + def call_addToPauseWhitelist(self, address_list): + return self._call("addToPauseWhitelist", [address_list], 0) + + def call_removeFromPauseWhitelist(self, address_list): + return self._call("removeFromPauseWhitelist", [address_list], 0) + + def call_pause(self): + return self._call("pause", [], 0) + + def call_resume(self): + return self._call("resume", [], 0) + + def query_getState(self): return self._query("getState", [], [State_decoder]) diff --git a/gen_pair_contract.py b/gen_pair_contract.py index b4d4032..c20dde6 100644 --- a/gen_pair_contract.py +++ b/gen_pair_contract.py @@ -4,7 +4,7 @@ # Example of command: -# `python3 gen_pair_contract.py pair.abi.json contracts/pair_contract_gen.py` +# `python3 gen_pair_contract.py abis/pair.abi.json contracts/pair_contract_gen.py` def abi_type_to_decoder_code(type: str): @@ -47,7 +47,7 @@ def abi_type_to_decoder_code(type: str): code = """from multiversx_sdk_core import Address -from utils.contract_interactor import ContractInteractor +from utils.contract_interactor import ContractInteractor, Payment from utils.decode import d @@ -58,27 +58,37 @@ def abi_type_to_decoder_code(type: str): code += f"class {contract_name}ContractInteractor(ContractInteractor):\n" for e_info in data["endpoints"]: - if e_info["mutability"] != "readonly": - continue e_name = e_info["name"] - inputs_code = "" + args_args_code = "" arg_codes = [] for i_info in e_info["inputs"]: - inputs_code += ", " + i_info["name"] + args_args_code += ", " + i_info["name"] i_type = i_info["type"] if i_type == "TokenIdentifier": - inputs_code += ": str" + args_args_code += ": str" elif i_type == "Address": - inputs_code += ": Address" + args_args_code += ": Address" elif i_type == "BigUint": - inputs_code += ": int" + args_args_code += ": int" arg_codes.append(i_info["name"]) - args_code = ", ".join(arg_codes) - decoder_codes = [] - for o_info in e_info["outputs"]: - decoder_codes.append(abi_type_to_decoder_code(o_info["type"])) - decoders_code = ", ".join(decoder_codes) - code += f"\tdef {e_name}(self{inputs_code}):\n\t\treturn self._query(\"{e_name}\", [{args_code}], [{decoders_code}])\n\n" + args_call_code = ", ".join(arg_codes) + if e_info["mutability"] == "readonly": + decoder_codes = [] + for o_info in e_info["outputs"]: + decoder_codes.append(abi_type_to_decoder_code(o_info["type"])) + decoders_code = ", ".join(decoder_codes) + code += f"\tdef query_{e_name}(self{args_args_code}):\n" + code += f"\t\treturn self._query(\"{e_name}\", [{args_call_code}], [{decoders_code}])\n\n" + elif e_info["mutability"] == "mutable": + payment_args_code = "" + payment_call_code = "" + if "payableInTokens" in e_info: + payment_args_code += ", _payment: Payment" + payment_call_code += ", _payment" + code += f"\tdef call_{e_name}(self{args_args_code}{payment_args_code}):\n" + code += f"\t\treturn self._call(\"{e_name}\", [{args_call_code}], 0{payment_call_code})\n\n" + else: + raise ValueError("Unknown mutability.") code += "\n" diff --git a/tmp.py b/tmp.py deleted file mode 100644 index 77479fd..0000000 --- a/tmp.py +++ /dev/null @@ -1,12 +0,0 @@ -from multiversx_sdk_network_providers import ProxyNetworkProvider -from multiversx_sdk_core import Address - -from contracts.pair_contract_gen import PairContractInteractor - - -proxy = ProxyNetworkProvider("https://gateway.multiversx.com") -contract_address = Address.from_bech32("erd1qqqqqqqqqqqqqpgqp5d4x3d263x4alnapwafwujch5xqmvyq2jpsk2xhsy") -interactor = PairContractInteractor(proxy, contract_address) - -print(interactor.getState()) -print(interactor.getTokensForGivenPosition(1000)) diff --git a/utils/contract_interactor.py b/utils/contract_interactor.py index bd467f8..02689fd 100644 --- a/utils/contract_interactor.py +++ b/utils/contract_interactor.py @@ -1,21 +1,32 @@ -from typing import Sequence +from typing import List, Optional, Sequence, Union import base64 from multiversx_sdk_network_providers import ProxyNetworkProvider from multiversx_sdk_core import Address, ContractQueryBuilder +from multiversx_sdk_core.transaction_builders import ContractCallBuilder, DefaultTransactionBuildersConfiguration +from utils.logger import get_logger from utils.decode import top_dec_bytes, Decoder +from utils.utils_chain import Account +from utils.utils_tx import send_contract_call_tx, _prep_args_for_addresses, ESDTToken + + +logger = get_logger(__name__) + +Payment = Union[int, ESDTToken, Sequence[ESDTToken]] class ContractInteractor: proxy: ProxyNetworkProvider - contract_address: Address + contract: Address + user: Optional[Account] - def __init__(self, proxy: ProxyNetworkProvider, contract_address: Address): + def __init__(self, proxy: ProxyNetworkProvider, contract: Address, user: Optional[Account] = None): self.proxy = proxy - self.contract_address = contract_address + self.contract = contract + self.user = user - def _query(self, function: str, args: list, decoders: Sequence[Decoder]): - query = ContractQueryBuilder(self.contract_address, function, args).build() + def _query(self, function: str, args: list, decoders: List[Decoder]): + query = ContractQueryBuilder(self.contract, function, args).build() b64_data = self.proxy.query_contract(query).return_data dec_data = [] for i in range(len(b64_data)): @@ -23,3 +34,37 @@ def _query(self, function: str, args: list, decoders: Sequence[Decoder]): decoder = decoders[min(i, len(decoders) - 1)] dec_data.append(top_dec_bytes(base64.b64decode(b64), decoder)) return dec_data + + def _call(self, function: str, args: Sequence, gas_limit: int, payment: Optional[Payment] = None): + logger.debug(f"Calling {function} at {self.contract.bech32()}") + logger.debug(f"With args: {args}") + + value: int | None = None + esdt_transfers: Sequence[ESDTToken] = [] + if isinstance(payment, int): + value = payment + elif isinstance(payment, ESDTToken): + esdt_transfers = [payment] + elif isinstance(payment, list) and all(isinstance(x, ESDTToken) for x in payment): + esdt_transfers = payment + + network_config = self.proxy.get_network_config() # TODO: find solution to avoid this call + config = DefaultTransactionBuildersConfiguration(chain_id=network_config.chain_id) + builder = ContractCallBuilder( + config=config, + caller=self.user.address, + contract=self.contract, + function_name=function, + call_arguments=_prep_args_for_addresses(args), + value=value, + esdt_transfers=esdt_transfers, + gas_limit=gas_limit, + nonce=self.user.nonce, + ) + tx = builder.build() + tx.signature = self.user.sign_transaction(tx) + + tx_hash = send_contract_call_tx(tx, self.proxy) + self.user.nonce += 1 if tx_hash != "" else 0 + + return tx_hash