def test_cli_name_claim(chain_fixture, tempdir): account_alice_path = _account_path(tempdir, chain_fixture.ALICE) # get a domain that is not under auction scheme domain = random_domain( length=13, tld='chain' if chain_fixture.NODE_CLI.get_consensus_protocol_version() >= identifiers.PROTOCOL_LIMA else 'test') # let alice preclaim a name j = call_aecli('name', 'pre-claim', '--password', 'aeternity_bc', account_alice_path, domain, '--wait') # retrieve the salt and the transaction hash salt = j.get("metadata", {}).get("salt") preclaim_hash = j.get("hash") # test that they are what we expect to be assert (isinstance(salt, int)) assert (salt > 0) assert (utils.is_valid_hash(preclaim_hash, identifiers.TRANSACTION_HASH)) # wait for confirmation chain_fixture.NODE_CLI.wait_for_confirmation(preclaim_hash) # now run the claim j = call_aecli('name', 'claim', account_alice_path, domain, '--password', 'aeternity_bc', '--name-salt', f"{salt}", '--preclaim-tx-hash', preclaim_hash, '--wait') assert (utils.is_valid_hash(j.get("hash"), identifiers.TRANSACTION_HASH)) # now run the name update j = call_aecli('name', 'update', '--password', 'aeternity_bc', account_alice_path, domain, chain_fixture.ALICE.get_address()) assert (utils.is_valid_hash(j.get("hash"), identifiers.TRANSACTION_HASH)) # now inspect the name j = call_aecli('inspect', domain) pointers = j.get('pointers', []) assert (len(pointers) == 1) assert (pointers[0]['id'] == chain_fixture.ALICE.get_address())
def test_cli_contract_deploy_call(chain_fixture, compiler_fixture, tempdir): node_cli = chain_fixture.NODE_CLI account_alice_path = _account_path(tempdir, chain_fixture.ALICE) # the contract c_src = "contract Identity =\n entrypoint main(x : int) = x" c_deploy_function = "init" c_call_function = "main" c_call_function_param = 42 # compile the contract compiler = compiler_fixture.COMPILER # compile and encode calldatas c_bin = compiler.compile(c_src).bytecode c_init_calldata = compiler.encode_calldata(c_src, c_deploy_function).calldata c_call_calldata = compiler.encode_calldata(c_src, c_call_function, c_call_function_param).calldata # write the contract to a file and execute the command contract_bin_path = os.path.join(tempdir, 'contract.bin') with open(contract_bin_path, "w") as fp: fp.write(c_bin) # deploy the contract j = call_aecli("contract", "deploy", "--wait", "--password", "aeternity_bc", account_alice_path, contract_bin_path, "--calldata", c_init_calldata) c_id = j.get("metadata", {}).get("contract_id") assert utils.is_valid_hash(c_id, prefix=identifiers.CONTRACT_ID) # now call j = call_aecli("contract", "call", "--wait", "--password", "aeternity_bc", account_alice_path, c_id, c_call_function, "--calldata", c_call_calldata) th = j.get("hash") assert utils.is_valid_hash(th, prefix=identifiers.TRANSACTION_HASH)
def test_cli_generate_account_and_account_info(tempdir): account_path = os.path.join(tempdir, 'key') j = call_aecli('account', 'create', account_path, '--password', 'secret') gen_address = j.get("Address") assert utils.is_valid_hash(gen_address, prefix='ak') j1 = call_aecli('account', 'address', account_path, '--password', 'secret') assert utils.is_valid_hash(j1.get('Address'), prefix='ak')
def test_cli_name_auction(chain_fixture, tempdir): if chain_fixture.NODE_CLI.get_consensus_protocol_version( ) < identifiers.PROTOCOL_LIMA: pytest.skip("name auction is only supported after Lima HF") return node_cli = chain_fixture.NODE_CLI account_alice_path = _account_path(tempdir, chain_fixture.ALICE) account_bob_path = _account_path(tempdir, chain_fixture.BOB) # get a domain that is under auction scheme domain = random_domain( length=9, tld='chain' if chain_fixture.NODE_CLI.get_consensus_protocol_version() >= identifiers.PROTOCOL_LIMA else 'test') # let alice preclaim a name j = call_aecli('name', 'pre-claim', '--password', 'aeternity_bc', account_alice_path, domain, '--wait') # retrieve the salt and the transaction hash salt = j.get("metadata", {}).get("salt") preclaim_hash = j.get("hash") # test that they are what we expect to be assert (isinstance(salt, int)) assert (salt > 0) assert (utils.is_valid_hash(preclaim_hash, identifiers.TRANSACTION_HASH)) # wait for confirmation chain_fixture.NODE_CLI.wait_for_confirmation(preclaim_hash) # now run the claim j = call_aecli('name', 'claim', account_alice_path, domain, '--password', 'aeternity_bc', '--name-salt', f"{salt}", '--preclaim-tx-hash', preclaim_hash, '--wait') assert (utils.is_valid_hash(j.get("hash"), identifiers.TRANSACTION_HASH)) # check that the tx was mined claim_height = chain_fixture.NODE_CLI.get_transaction_by_hash( hash=j.get('hash')).block_height assert (isinstance(claim_height, int) and claim_height > 0) # now we have a first claim # this is the name fee and the end block name_fee = AEName.compute_bid_fee(domain) aucion_end = AEName.compute_auction_end_block(domain, claim_height) print( f"Name {domain}, name_fee {name_fee}, claim height: {claim_height}, auction_end: {aucion_end}" ) # now make another bid # first compute the new name fee name_fee = AEName.compute_bid_fee(domain, name_fee) j = call_aecli('name', 'bid', account_bob_path, domain, f'{name_fee}', '--password', 'aeternity_bc', '--wait') assert (utils.is_valid_hash(j.get("hash"), identifiers.TRANSACTION_HASH)) # check that the tx was mined claim_height = chain_fixture.NODE_CLI.get_transaction_by_hash( hash=j.get('hash')).block_height assert (isinstance(claim_height, int) and claim_height > 0) aucion_end = AEName.compute_auction_end_block(domain, claim_height) print( f"Name {domain}, name_fee {name_fee}, claim height: {claim_height}, auction_end: {aucion_end}" ) name = chain_fixture.NODE_CLI.AEName(domain) # name should still be available assert (name.is_available())
def tx_call(self, keypair, function, arg, amount=10, gas=CONTRACT_DEFAULT_GAS, gas_price=CONTRACT_DEFAULT_GAS_PRICE, fee=DEFAULT_FEE, vm_version=CONTRACT_DEFAULT_VM_VERSION, tx_ttl=DEFAULT_TX_TTL): """Call a sophia contract""" if not utils.is_valid_hash(self.address, prefix="ct"): raise ValueError("Missing contract id") try: call_data = self.encode_calldata(function, arg) txb = TxBuilder(self.client, keypair) tx, sg, tx_hash = txb.tx_contract_call(self.address, call_data, function, arg, amount, gas, gas_price, vm_version, fee, tx_ttl) # post the transaction to the chain txb.post_transaction(tx, tx_hash) # wait for transaction to be mined txb.wait_tx(tx_hash) # unsigned transaction of the call call_obj = self.client.cli.get_transaction_info_by_hash( hash=tx_hash) return call_obj except OpenAPIClientException as e: raise ContractError(e)
def get_transaction(self, transaction_hash): # TODO: continue if not utils.is_valid_hash(transaction_hash, identifiers.TRANSACTION_HASH): raise TypeError( f"Input {transaction_hash} is not a valid aeternity address") tx = self.get_transaction_by_hash(hash=transaction_hash) return self.tx_builder.parse_node_reply(tx)
def __prepare_call_tx(self, contract_id, address, function, calldata, amount, gas, gas_price, fee, abi_version, tx_ttl): """ Prepare a Contract call transaction :param contract_id: the contract address :param address: the address preparing the transaction """ if not utils.is_valid_hash(contract_id, prefix=identifiers.CONTRACT_ID): raise ValueError(f"Invalid contract id {contract_id}") # parse amounts amount, gas_price, fee = utils._amounts_to_aettos( amount, gas_price, fee) # check if the contract exists try: self.client.get_contract(pubkey=contract_id) except openapi.OpenAPIClientException: raise ContractError(f"Contract {contract_id} not found") try: # retrieve the correct vm/abi version _, abi = self.client.get_vm_abi_versions() abi_version = abi if abi_version is None else abi_version # get the transaction builder txb = self.client.tx_builder # get the account nonce and ttl nonce, ttl = self.client._get_nonce_ttl(address, tx_ttl) # build the transaction return txb.tx_contract_call(address, contract_id, calldata, function, amount, gas, gas_price, abi_version, fee, ttl, nonce) except openapi.OpenAPIClientException as e: raise ContractError(e)
def get_account(self, address: str) -> Account: """ Retrieve an account by it's public key """ if not utils.is_valid_hash(address, identifiers.ACCOUNT_ID): raise TypeError(f"Input {address} is not a valid aeternity address") remote_account = self.get_account_by_pubkey(pubkey=address) return Account.from_node_api(remote_account)
def _get_pointers(self, targets): """ Create a list of pointers given a list of addresses """ pointers = [] for t in targets: if isinstance(t, tuple): # custom target pointers.append({'key': t[0], 'id': t[1]}) elif utils.is_valid_hash(t, prefix=identifiers.ACCOUNT_ID): pointers.append({'id': t, 'key': 'account_pubkey'}) elif utils.is_valid_hash(t, prefix=identifiers.ORACLE_ID): pointers.append({'id': t, 'key': 'oracle_pubkey'}) elif utils.is_valid_hash(t, prefix=identifiers.CONTRACT_ID): pointers.append({'id': t, 'key': 'contract_pubkey'}) else: raise TypeError(f"invalid aens update pointer target {t}") return pointers
def test_cli_generate_account(tempdir): account_key = os.path.join(tempdir, 'key') j = call_aecli('account', 'create', account_key, '--password', 'secret', '--overwrite') gen_address = j.get("Address") assert utils.is_valid_hash(gen_address, prefix='ak') # make sure the folder contains the keys files = sorted(os.listdir(tempdir)) assert len(files) == 1 assert files[0].startswith("key")
def at(self, address): """ Set contract address """ if not address or not utils.is_valid_hash( address, prefix=identifiers.CONTRACT_ID): raise ValueError(f"Invalid contract address {address}") if not self.contract.is_deployed(address): raise ValueError("Contract not deployed") self.address = address self.deployed = True
def get_transaction(self, transaction_hash: str) -> transactions.TxObject: """ Retrieve a transaction by it's hash. Args: transaction_hash: the hash of the transaction to retrieve Returns: the TxObject of the transaction Raises: ValueError: if the transaction hash is not a valid hash for transactions """ if not utils.is_valid_hash(transaction_hash, identifiers.TRANSACTION_HASH): raise ValueError(f"Input {transaction_hash} is not a valid aeternity address") tx = self.get_transaction_by_hash(hash=transaction_hash) return self.tx_builder.parse_node_reply(tx)
def test_signing_is_valid_hash(): # input (hash_str, prefix, expected output) args = [ ('ak_me6L5SSXL4NLWv5EkQ7a16xaA145Br7oV4sz9JphZgsTsYwGC', None, True), ('ak_me6L5SSXL4NLWv5EkQ7a16xaA145Br7oV4sz9JphZgsTsYwGC', 'ak', True), ('ak_me6L5SSXL4NLWv5EkQ7a16xaA145Br7oV4sz9JphZgsTsYwGC', 'bh', False), ('ak_me6L5SSXL4NLWv5EkQ7a16xaA145Br7oV4sz9JphZgsTsYwYC', None, False), ('ak_me6L5SSXL4NLWv5EkQ7a18xaA145Br7oV4sz9JphZgsTsYwGC', None, False), ('bh_vzUC2jVuAfpBC3tMAHhxwxJnTFymckNYeQ5TWZua1pydabqNu', None, True), ('th_YqPSTzs73PiKFhFcALYWWu41uNLc6yp63ZC35jzzuJYA9PMui', None, True), ] for a in args: got = is_valid_hash(a[0], a[1]) expected = a[2] assert got == expected
def call(self, contract_id, account, function, arg, calldata, amount=defaults.CONTRACT_AMOUNT, gas=defaults.CONTRACT_GAS, gas_price=defaults.CONTRACT_GAS_PRICE, fee=defaults.FEE, abi_version=None, tx_ttl=defaults.TX_TTL): """Call a sophia contract""" if not utils.is_valid_hash(contract_id, prefix=identifiers.CONTRACT_ID): raise ValueError(f"Invalid contract id {contract_id}") # check if the contract exists try: self.client.get_contract(pubkey=contract_id) except openapi.OpenAPIClientException: raise ContractError(f"Contract {contract_id} not found") try: # retrieve the correct vm/abi version _, abi = self.client.get_vm_abi_versions() abi_version = abi if abi_version is None else abi_version # get the transaction builder txb = self.client.tx_builder # get the account nonce and ttl nonce, ttl = self.client._get_nonce_ttl(account.get_address(), tx_ttl) # build the transaction tx = txb.tx_contract_call(account.get_address(), self.address, calldata, function, arg, amount, gas, gas_price, abi_version, fee, ttl, nonce) # sign the transaction tx_signed = self.client.sign_transaction(account, tx.tx) # post the transaction to the chain self.client.broadcast_transaction(tx_signed.tx, tx_signed.hash) return tx_signed except openapi.OpenAPIClientException as e: raise ContractError(e)
def spend(self, account: Account, recipient_id: str, amount, payload: str = "", fee: int = defaults.FEE, tx_ttl: int = defaults.TX_TTL) -> transactions.TxObject: """ Create and execute a spend transaction, automatically retrieve the nonce for the siging account and calculate the absolut ttl. :param account: the account signing the spend transaction (sender) :param recipient_id: the recipient address or name_id :param amount: the amount to spend :param payload: the payload for the transaction :param fee: the fee for the transaction (automatically calculated if not provided) :param tx_ttl: the transaction ttl expressed in relative number of blocks :return: the TxObject of the transaction :raises TypeError: if the recipient_id is not a valid name_id or address """ if utils.is_valid_aens_name(recipient_id): recipient_id = hashing.name_id(recipient_id) elif not utils.is_valid_hash(recipient_id, prefix="ak"): raise TypeError("Invalid recipient_id. Please provide a valid AENS name or account pub_key.") # parse amount and fee amount, fee = utils._amounts_to_aettos(amount, fee) # retrieve the nonce account.nonce = self.get_next_nonce(account.get_address()) if account.nonce == 0 else account.nonce + 1 # retrieve ttl tx_ttl = self.compute_absolute_ttl(tx_ttl) # build the transaction tx = self.tx_builder.tx_spend(account.get_address(), recipient_id, amount, payload, fee, tx_ttl.absolute_ttl, account.nonce) # get the signature tx = self.sign_transaction(account, tx) # post the signed transaction transaction self.broadcast_transaction(tx) return tx
def transfer_funds(self, account: Account, recipient_id: str, percentage: float, payload: str = "", tx_ttl: int = defaults.TX_TTL, fee: int = defaults.FEE, include_fee=True): """ Create and execute a spend transaction """ if utils.is_valid_aens_name(recipient_id): recipient_id = hashing.name_id(recipient_id) elif not utils.is_valid_hash(recipient_id, prefix="ak"): raise TypeError("Invalid recipient_id. Please provide a valid AENS name or account pub_key.") if percentage < 0 or percentage > 1: raise ValueError(f"Percentage should be a number between 0 and 1, got {percentage}") # parse amounts fee = utils.amount_to_aettos(fee) # retrieve the balance account_on_chain = self.get_account_by_pubkey(pubkey=account.get_address()) request_transfer_amount = int(account_on_chain.balance * percentage) # retrieve the nonce account.nonce = account_on_chain.nonce + 1 # retrieve ttl tx_ttl = self.compute_absolute_ttl(tx_ttl) # build the transaction tx = self.tx_builder.tx_spend(account.get_address(), recipient_id, request_transfer_amount, payload, fee, tx_ttl.absolute_ttl, account.nonce) # if the request_transfer_amount should include the fee keep calculating the fee if include_fee: amount = request_transfer_amount while (amount + tx.data.fee) > request_transfer_amount: amount = request_transfer_amount - tx.data.fee tx = self.tx_builder.tx_spend(account.get_address(), recipient_id, amount, payload, fee, tx_ttl.absolute_ttl, account.nonce) # execute the transaction tx = self.sign_transaction(account, tx) # post the transaction self.broadcast_transaction(tx) return tx
def tx_call(self, account, function, arg, amount=defaults.CONTRACT_AMOUNT, gas=defaults.CONTRACT_GAS, gas_price=defaults.CONTRACT_GAS_PRICE, fee=defaults.FEE, vm_version=None, abi_version=None, tx_ttl=defaults.TX_TTL): """Call a sophia contract""" if not utils.is_valid_hash(self.address, prefix=CONTRACT_ID): raise ValueError("Missing contract id") try: # retrieve the correct vm/abi version vm, abi = self._get_vm_abi_versions() vm_version = vm if vm_version is None else vm_version abi_version = abi if abi_version is None else abi_version # prepare the call data call_data = self.encode_calldata(function, arg) # get the transaction builder txb = self.client.tx_builder # get the account nonce and ttl nonce, ttl = self.client._get_nonce_ttl(account.get_address(), tx_ttl) # build the transaction tx = txb.tx_contract_call(account.get_address(), self.address, call_data, function, arg, amount, gas, gas_price, abi_version, fee, ttl, nonce) # sign the transaction tx_signed = self.client.sign_transaction(account, tx.tx) # post the transaction to the chain self.client.broadcast_transaction(tx_signed.tx, tx_signed.hash) # unsigned transaction of the call call_obj = self.client.get_transaction_info_by_hash(hash=tx_signed.hash) return tx_signed, call_obj except OpenAPIClientException as e: raise ContractError(e)
def _raw_key(cls, key_string): """decode a key with different method between signing and addresses""" key_string = str(key_string) if utils.is_valid_hash(key_string, prefix=ACCOUNT_ID): return hashing.decode(key_string.strip()) return bytes.fromhex(key_string.strip())
def test_evm_contract_compile(): contract = EPOCH_CLI.Contract(aer_identity_contract, abi=Contract.EVM) print(contract) assert contract.bytecode is not None assert utils.is_valid_hash(contract.bytecode, prefix='cb')
def test_sophia_encode_calldata(): contract = EPOCH_CLI.Contract(aer_identity_contract) result = contract.encode_calldata('main', '1') assert result is not None assert utils.is_valid_hash(result, prefix='cb')
def test_sophia_contract_compile(): contract = EPOCH_CLI.Contract(aer_identity_contract) assert contract is not None utils.is_valid_hash(contract.bytecode, prefix='cb')
def test_sophia_contract_compile(chain_fixture): contract = chain_fixture.NODE_CLI.Contract(aer_identity_contract) assert contract is not None utils.is_valid_hash(contract.bytecode, prefix='cb')
def rest_faucet(recipient_address): """top up an account""" notification_message = "" try: # validate the address app.logger.info(f"Top up request for {recipient_address}") if not is_valid_hash(recipient_address, prefix='ak'): notification_message = "The provided account is not valid" return jsonify({"message": notification_message}), 400 # check if the account is still in the cache registration_date = app.config['address_cache'].get(recipient_address) if registration_date is not None: graylist_exp = registration_date + timedelta( seconds=app.config['cache_max_age']) notification_message = f"The account `{recipient_address}` is graylisted for another {pretty_time_delta(graylist_exp, datetime.now())}" msg = f"The account is graylisted for another {pretty_time_delta(graylist_exp, datetime.now())}" return jsonify({"message": msg}), 425 app.config['address_cache'][recipient_address] = datetime.now() # sender account sender = signing.Account.from_private_key_string( FAUCET_ACCOUNT_PRIV_KEY) # execute the spend transaction client = app.config.get("node_client") tx = client.spend(sender, recipient_address, TOPUP_AMOUNT, payload=SPEND_TX_PAYLOAD) # print the full transaction balance = client.get_account_by_pubkey( pubkey=recipient_address).balance app.logger.info( f"Top up account {recipient_address} of {TOPUP_AMOUNT} tx_hash: {tx.hash} completed" ) app.logger.debug(f"tx: {tx.tx}") # notifications node = NODE_URL.replace("https://", "") notification_message = f"Account `{recipient_address}` credited with {format_amount(TOPUP_AMOUNT)} tokens on `{node}`. (tx hash: `{tx}`)" # return return jsonify({"tx_hash": tx.hash, "balance": balance}) except OpenAPIClientException as e: app.logger.error( f"API error: top up account {recipient_address} of {TOPUP_AMOUNT} failed with error", e) # notifications node = NODE_URL.replace("https://", "") notification_message = f"API error: top up account {recipient_address} of {TOPUP_AMOUNT} on {node} failed with error {e}" return jsonify({ "message": "The node is temporarily unavailable, please try again later" }), 503 except Exception as e: app.logger.error( f"Generic error: top up account {recipient_address} of {TOPUP_AMOUNT} failed with error", e) # notifications node = NODE_URL.replace("https://", "") notification_message = f"API error: top up account {recipient_address} of {TOPUP_AMOUNT} on {node} failed with error {e}" return jsonify({ "message": f"""Unknown error, please contact <a href="{SUPPORT_EMAIL}" class="hover:text-pink-lighter">{SUPPORT_EMAIL}</a>""" }), 500 finally: try: # telegram bot notifications if TELEGRAM_API_TOKEN: if TELEGRAM_CHAT_ID is None or TELEGRAM_API_TOKEN is None: app.logger.warning( f"missing chat_id ({TELEGRAM_CHAT_ID}) or token {TELEGRAM_API_TOKEN} for telegram integration" ) bot = telegram.Bot(token=TELEGRAM_API_TOKEN) bot.send_message(chat_id=TELEGRAM_CHAT_ID, text=notification_message, parse_mode=telegram.ParseMode.MARKDOWN) except Exception as e: app.logger.error(f"Error delivering notifications", e)
def rest_faucet(recipient_address): """top up an account""" amount = int(os.environ.get('TOPUP_AMOUNT', 250)) ttl = int(os.environ.get('TX_TTL', 100)) try: # validate the address logging.info(f"Top up request for {recipient_address}") if not is_valid_hash(recipient_address, prefix='ak'): return jsonify({"message": "The provided account is not valid"}), 400 # genesys key bank_wallet_key = os.environ.get('FAUCET_ACCOUNT_PRIV_KEY') kp = Account.from_private_key_string(bank_wallet_key) # target node Config.set_defaults( Config( external_url=os.environ.get('EPOCH_URL', "https://sdk-testnet.aepps.com"), internal_url=os.environ.get('EPOCH_URL_DEBUG', "https://sdk-testnet.aepps.com"), network_id=os.environ.get('NETWORK_ID', "ae_devnet"), )) # payload payload = os.environ.get('TX_PAYLOAD', "Faucet Tx") # execute the spend transaction client = EpochClient() _, _, _, tx = client.spend(kp, recipient_address, amount, payload=payload, tx_ttl=ttl) balance = client.get_account_by_pubkey( pubkey=recipient_address).balance logging.info( f"Top up accont {recipient_address} of {amount} tx_ttl: {ttl} tx_hash: {tx} completed" ) # telegram bot notifications enable_telegaram = os.environ.get('TELEGRAM_API_TOKEN', False) if enable_telegaram: token = os.environ.get('TELEGRAM_API_TOKEN', None) chat_id = os.environ.get('TELEGRAM_CHAT_ID', None) node = os.environ.get('EPOCH_URL', "https://sdk-testnet.aepps.com").replace( "https://", "") if token is None or chat_id is None: logging.warning( f"missing chat_id ({chat_id}) or token {token} for telegram integration" ) bot = telegram.Bot(token=token) bot.send_message( chat_id=chat_id, text= f"Account `{recipient_address}` credited with {amount} tokens on `{node}`. (tx hash: `{tx}`)", parse_mode=telegram.ParseMode.MARKDOWN) return jsonify({"tx_hash": tx, "balance": balance}) except OpenAPIClientException as e: logging.error( f"Api error: top up accont {recipient_address} of {amount} failed with error", e) return jsonify({ "message": "The node is temporarily unavailable, contact aepp-dev[at]aeternity.com" }), 503 except Exception as e: logging.error( f"Generic error: top up accont {recipient_address} of {amount} failed with error", e) return jsonify({ "message": "Unknow error, please contact contact aepp-dev[at]aeternity.com" }), 500
def to_sophia_bytes(self, arg, generic, bindings={}): if isinstance(arg, str): val = hashing.decode(arg).hex() if utils.is_valid_hash(arg) else arg return f'#{val}' elif isinstance(arg, bytes): return f"#{arg.hex()}"
def test_cli_inspect_key_block_by_height(chain_fixture): height = chain_fixture.NODE_CLI.get_current_key_block_height() j = call_aecli('inspect', str(height)) assert utils.is_valid_hash(j.get("hash"), prefix=["kh", "mh"]) assert j.get("height") == height