def unlock(aergo_to: herapy.Aergo, receiver: str, burn_proof: AttributeDict, token_origin: str, bridge_to: str, gas_limit: int, gas_price: int) -> Tuple[str, Transaction]: """ Unlock the receiver's deposit balance on aergo_to. """ if not is_aergo_address(receiver): raise InvalidArgumentsError( "Receiver {} must be an Aergo address".format(receiver)) if not is_aergo_address(token_origin): raise InvalidArgumentsError( "token_origin {} must be an Aergo address".format(token_origin)) ap = format_proof_for_lua(burn_proof.storageProof[0].proof) balance = int.from_bytes(burn_proof.storageProof[0].value, "big") ubig_balance = {'_bignum': str(balance)} # call unlock on aergo_to with the burn proof from aergo_from tx, result = aergo_to.call_sc( bridge_to, "unlock", args=[receiver, ubig_balance, token_origin, ap]) if result.status != herapy.CommitStatus.TX_OK: raise TxError("Unlock asset Tx commit failed : {}".format(result)) result = aergo_to.wait_tx_result(tx.tx_hash) if result.status != herapy.TxResultStatus.SUCCESS: raise TxError("Unlock asset Tx execution failed : {}".format(result)) logger.info("\u26fd Aergo gas used: %s", result.gas_used) return str(tx.tx_hash), result
def get_asset_address(self, asset_name, from_chain, to_chain): try: addr = self.wallet.config_data( 'networks', from_chain, 'tokens', asset_name, 'addr') return addr except KeyError: pass try: addr = self.wallet.config_data( 'networks', to_chain, 'tokens', asset_name, 'pegs', from_chain) return addr except KeyError: pass raise InvalidArgumentsError( 'asset not properly registered in config.json')
def build_burn_proof( w3: Web3, aergo_to: herapy.Aergo, receiver: str, bridge_from: str, bridge_to: str, burn_height: int, token_origin: str, ): """ Check the last anchored root includes the lock and build a lock proof for that root """ if not is_aergo_address(receiver): raise InvalidArgumentsError( "Receiver {} must be an Aergo address".format(receiver)) if not is_aergo_address(token_origin): raise InvalidArgumentsError( "token_origin {} must be an Aergo address".format(token_origin)) account_ref = (receiver + token_origin).encode('utf-8') # 'Burns is the 8th state var defined in solitity contract position = b'\x07' trie_key = keccak(account_ref + position.rjust(32, b'\0')) return _build_deposit_proof(w3, aergo_to, bridge_from, bridge_to, burn_height, trie_key)
def burn_to_eth( self, from_chain: str, to_chain: str, asset_name: str, amount: int, receiver: str, privkey_name: str = 'default', privkey_pwd: str = None, ) -> Tuple[int, str]: """ Initiate minted token transfer back to ethereum origin""" logger.info(from_chain + ' -> ' + to_chain) if not is_ethereum_address(receiver): raise InvalidArgumentsError( "receiver {} must be an Ethereum address".format(receiver)) aergo_from = self.get_aergo(from_chain, privkey_name, privkey_pwd) sender = str(aergo_from.account.address) bridge_from = self.get_bridge_contract_address(from_chain, to_chain) token_pegged = self.get_asset_address(asset_name, from_chain, asset_origin_chain=to_chain) gas_limit = 300000 balance = aergo_u.get_balance(sender, token_pegged, aergo_from) if balance < amount: raise InsufficientBalanceError("not enough token balance") logger.info("\U0001f4b0 %s balance on origin before transfer: %s", asset_name, balance / 10**18) aer_balance = aergo_u.get_balance(sender, 'aergo', aergo_from) if aer_balance < gas_limit * self.aergo_gas_price: err = "not enough aer balance to pay tx fee" raise InsufficientBalanceError(err) burn_height, tx_hash, _ = aergo_to_eth.burn(aergo_from, bridge_from, receiver, amount, token_pegged, gas_limit, self.aergo_gas_price) logger.info('\U0001f525 Burn success: %s', tx_hash) # remaining balance on origin : aer or asset balance = aergo_u.get_balance(sender, token_pegged, aergo_from) logger.info( "\U0001f4b0 remaining %s balance on origin after transfer: %s", asset_name, balance / 10**18) aergo_from.disconnect() return burn_height, tx_hash
def unlock_to_aergo( self, from_chain: str, to_chain: str, asset_name: str, receiver: str, burn_height: int = 0, privkey_name: str = 'default', privkey_pwd: str = None, ) -> str: """ Finalize Aergo Standard token transfer back to Aergo Origin""" logger.info(from_chain + ' -> ' + to_chain) if not is_aergo_address(receiver): raise InvalidArgumentsError( "Receiver {} must be an Aergo address".format(receiver)) w3 = self.get_web3(from_chain) aergo_to = self.get_aergo(to_chain, privkey_name, privkey_pwd) tx_sender = str(aergo_to.account.address) bridge_to = self.get_bridge_contract_address(to_chain, from_chain) bridge_from = self.get_bridge_contract_address(from_chain, to_chain) asset_address = self.get_asset_address(asset_name, to_chain) burn_proof = eth_to_aergo.build_burn_proof(w3, aergo_to, receiver, bridge_from, bridge_to, burn_height, asset_address) logger.info("\u2699 Built burn proof") balance = aergo_u.get_balance(receiver, asset_address, aergo_to) logger.info("\U0001f4b0 %s balance on destination before transfer: %s", asset_name, balance / 10**18) gas_limit = 300000 aer_balance = aergo_u.get_balance(tx_sender, 'aergo', aergo_to) if aer_balance < gas_limit * self.aergo_gas_price: err = "not enough aer balance to pay tx fee" raise InsufficientBalanceError(err) tx_hash, _ = eth_to_aergo.unlock(aergo_to, receiver, burn_proof, asset_address, bridge_to, gas_limit, self.aergo_gas_price) logger.info('\U0001f513 Unlock success: %s', tx_hash) # new balance on origin balance = aergo_u.get_balance(receiver, asset_address, aergo_to) logger.info("\U0001f4b0 %s balance on destination after transfer: %s", asset_name, balance / 10**18) return tx_hash
def get_abi_function(w3: Web3, asset_addr: str, abi: str, spender: str, amount: int): token_contract = w3.eth.contract(address=asset_addr, abi=abi) try: function = token_contract.functions.increaseAllowance(spender, amount) return function except MismatchedABI: pass try: function = token_contract.functions.increaseApproval(spender, amount) return function except MismatchedABI: pass raise InvalidArgumentsError( "Impossible to send ERC20 tokens to bridge contract: " "'increaseAllowance' or 'increaseApproval' must be included " "in ERC20 abi")
def _build_deposit_proof( aergo_from: herapy.Aergo, w3: Web3, bridge_from: str, bridge_to: str, bridge_to_abi: str, deposit_height: int, trie_key: bytes, ): """ Check the last anchored root includes the deposit and build a deposit proof for that root """ # check last merged height eth_bridge = w3.eth.contract(address=bridge_to, abi=bridge_to_abi) try: last_merged_height_to = eth_bridge.functions._anchorHeight().call() except BadFunctionCallOutput as e: raise InvalidArgumentsError(e, bridge_to) # waite for anchor containing our transfer if last_merged_height_to < deposit_height: logger.info( "\u23F0 deposit not recorded in current anchor, waiting new " "anchor event... / deposit height : %s / last anchor height : %s ", deposit_height, last_merged_height_to) while last_merged_height_to < deposit_height: time.sleep(1) last_merged_height_to = eth_bridge.functions._anchorHeight().call() # get inclusion proof of lock in last merged block merge_block_from = aergo_from.get_block_headers( block_height=last_merged_height_to, list_size=1) root_from = merge_block_from[0].blocks_root_hash proof = aergo_from.query_sc_state(bridge_from, [trie_key], root=root_from, compressed=True) if not proof.verify_proof(root_from): raise InvalidMerkleProofError("Unable to verify deposit proof", proof) if not proof.account.state_proof.inclusion: raise InvalidMerkleProofError( "Contract doesnt exist in state, check contract deployed and " "chain synced {}".format(proof)) if not proof.var_proofs[0].inclusion: raise InvalidMerkleProofError( "No tokens deposited for this account reference: {}".format(proof)) return proof
def get_balance_aergo(self, asset_name: str, network_name: str, asset_origin_chain: str = None, account_name: str = 'default', account_addr: str = None) -> Tuple[int, str]: """ Get account name balance of asset_name on network_name, and specify asset_origin_chain for a pegged asset query, """ if account_addr is None: account_addr = self.config_data('wallet', account_name, 'addr') if not is_aergo_address(account_addr): raise InvalidArgumentsError( "Account {} must be an Aergo address".format(account_addr)) aergo = self.connect_aergo(network_name) asset_addr = self.get_asset_address(asset_name, network_name, asset_origin_chain) balance = aergo_u.get_balance(account_addr, asset_addr, aergo) aergo.disconnect() return balance, asset_addr
def prompt_signing_key(self, wallet_name): """Prompt user to select a private key. Note: Keys are displayed by name and should have been registered in wallet config. """ accounts = self.wallet.config_data(wallet_name) if len(accounts) == 0: raise InvalidArgumentsError( "No private key available, first register a private key for " "signing transactions") questions = [{ 'type': 'list', 'name': 'privkey_name', 'message': 'Choose account to sign transaction : ', 'choices': [name for name in accounts] }] answers = inquirer.prompt(questions, style=aergo_style) return answers['privkey_name']
def freeze( self, from_chain: str, to_chain: str, amount: int, receiver: str, privkey_name: str = 'default', privkey_pwd: str = None, ) -> Tuple[int, str]: """ Initiate Aer transfer back to Ethereum AergoERC20 sidechain""" logger.info(from_chain + ' -> ' + to_chain) if not is_ethereum_address(receiver): raise InvalidArgumentsError( "receiver {} must be an Ethereum address".format(receiver)) asset_name = 'aergo_erc20' aergo_from = self.get_aergo(from_chain, privkey_name, privkey_pwd) sender = str(aergo_from.account.address) bridge_from = self.get_bridge_contract_address(from_chain, to_chain) gas_limit = 300000 balance = aergo_u.get_balance(sender, 'aergo', aergo_from) if balance < amount + gas_limit * self.aergo_gas_price: raise InsufficientBalanceError("not enough token balance") logger.info("\U0001f4b0 %s balance on origin before transfer: %s", asset_name, balance / 10**18) freeze_height, tx_hash, _ = aergo_to_eth.freeze( aergo_from, bridge_from, receiver, amount, gas_limit, self.aergo_gas_price) logger.info('\u2744 Freeze success: %s', tx_hash) # remaining balance on origin : aer or asset balance = aergo_u.get_balance(sender, 'aergo', aergo_from) logger.info( "\U0001f4b0 remaining %s balance on origin after transfer: %s", asset_name, balance / 10**18) aergo_from.disconnect() return freeze_height, tx_hash
def transfer( value: int, to: str, asset_addr: str, aergo: herapy.Aergo, sender: str, fee_limit: int, fee_price: int, ) -> str: """ Support 3 types of transfers : simple aer transfers, token transfer, and signed token transfers (token owner != tx signer) """ if not is_aergo_address(to): raise InvalidArgumentsError( "Receiver {} must be an Aergo address".format(to)) aergo.get_account() # get the latest nonce for making tx if asset_addr == "aergo": # transfer aer on network_name tx, result = aergo.send_payload(to_address=to, amount=value, payload=None) else: # transfer token (issued or pegged) on network_name tx, result = aergo.call_sc(asset_addr, "transfer", args=[to, { "_bignum": str(value) }], amount=0) if result.status != herapy.CommitStatus.TX_OK: raise TxError("Transfer asset Tx commit failed : {}".format(result)) # Check lock success result = aergo.wait_tx_result(tx.tx_hash) if result.status != herapy.TxResultStatus.SUCCESS: raise TxError("Transfer asset Tx execution failed : {}".format(result)) return str(tx.tx_hash)
def lock(w3: Web3, signer_acct, receiver: str, amount: int, bridge_from: str, bridge_from_abi: str, erc20_address: str, gas_limit: int, gas_price: int, next_nonce: int = None) -> Tuple[int, str, AttributeDict]: """ Lock an Ethereum ERC20 token. """ if not is_aergo_address(receiver): raise InvalidArgumentsError( "Receiver {} must be an Aergo address".format(receiver)) bridge_from = Web3.toChecksumAddress(bridge_from) eth_bridge = w3.eth.contract(address=bridge_from, abi=bridge_from_abi) if next_nonce is None: next_nonce = w3.eth.getTransactionCount(signer_acct.address) construct_txn = eth_bridge.functions.lock(erc20_address, amount, receiver).buildTransaction({ 'chainId': w3.eth.chainId, 'from': signer_acct.address, 'nonce': next_nonce, 'gas': gas_limit, 'gasPrice': w3.toWei(gas_price, 'gwei') }) signed = signer_acct.sign_transaction(construct_txn) tx_hash = w3.eth.sendRawTransaction(signed.rawTransaction) receipt = w3.eth.waitForTransactionReceipt(tx_hash) if receipt.status != 1: raise TxError("Lock asset Tx execution failed: {}".format(receipt)) logger.info("\u26fd Eth Gas used: %s", receipt.gasUsed) return receipt.blockNumber, tx_hash.hex(), receipt
def mint_to_eth( self, from_chain: str, to_chain: str, asset_name: str, receiver: str = None, lock_height: int = 0, privkey_name: str = 'default', privkey_pwd: str = None, ) -> Tuple[str, str]: """ Finalize Aergo Standard Token transfer to Ethereum sidechain NOTE anybody can mint so sender is not necessary. The amount to mint is the difference between total deposit and already minted amount. Bridge tempo is taken from config_data """ logger.info(from_chain + ' -> ' + to_chain) bridge_to_abi = self.load_bridge_abi(to_chain, from_chain) minted_erc20_abi = self.load_minted_erc20_abi(to_chain, from_chain) aergo_from = self.connect_aergo(from_chain) w3 = self.get_web3(to_chain) signer_acct = self.get_signer(w3, privkey_name, privkey_pwd) tx_sender = signer_acct.address if receiver is None: receiver = tx_sender if not is_ethereum_address(receiver): raise InvalidArgumentsError( "receiver {} must be an Ethereum address".format(receiver)) bridge_from = self.get_bridge_contract_address(from_chain, to_chain) bridge_to = self.get_bridge_contract_address(to_chain, from_chain) asset_address = self.get_asset_address(asset_name, from_chain) save_pegged_token_address = False try: token_pegged = self.get_asset_address( asset_name, to_chain, asset_origin_chain=from_chain) balance = eth_u.get_balance(receiver, token_pegged, w3, minted_erc20_abi) logger.info( "\U0001f4b0 %s balance on destination before transfer : %s", asset_name, balance / 10**18) except KeyError: logger.info("Pegged token unknow by wallet") save_pegged_token_address = True gas_limit = 2000000 eth_balance = eth_u.get_balance(tx_sender, 'ether', w3) if eth_balance * 10**9 < gas_limit * self.eth_gas_price: err = "not enough aer balance to pay tx fee" raise InsufficientBalanceError(err) lock_proof = aergo_to_eth.build_lock_proof(aergo_from, w3, receiver, bridge_from, bridge_to, bridge_to_abi, lock_height, asset_address) logger.info("\u2699 Built lock proof") token_pegged, tx_hash, _ = aergo_to_eth.mint(w3, signer_acct, receiver, lock_proof, asset_address, bridge_to, bridge_to_abi, gas_limit, self.eth_gas_price) logger.info('\u26cf Mint success: %s', tx_hash) # new balance on sidechain balance = eth_u.get_balance(receiver, token_pegged, w3, minted_erc20_abi) logger.info("\U0001f4b0 %s balance on destination after transfer : %s", asset_name, balance / 10**18) aergo_from.disconnect() # record mint address in file if save_pegged_token_address: logger.info("------ Store mint address in config.json -----------") self.config_data('networks', from_chain, 'tokens', asset_name, 'pegs', to_chain, value=token_pegged) self.save_config() return token_pegged, tx_hash
def mint_to_aergo( self, from_chain: str, to_chain: str, asset_name: str, receiver: str = None, lock_height: int = 0, privkey_name: str = 'default', privkey_pwd: str = None, ) -> str: """ Finalize ERC20 token or Ether transfer to Aergo sidechain """ logger.info(from_chain + ' -> ' + to_chain) w3 = self.get_web3(from_chain) aergo_to = self.get_aergo(to_chain, privkey_name, privkey_pwd) tx_sender = str(aergo_to.account.address) bridge_to = self.get_bridge_contract_address(to_chain, from_chain) bridge_from = self.get_bridge_contract_address(from_chain, to_chain) asset_address = self.get_asset_address(asset_name, from_chain) if receiver is None: receiver = tx_sender if not is_aergo_address(receiver): raise InvalidArgumentsError( "Receiver {} must be an Aergo address".format(receiver)) save_pegged_token_address = False try: token_pegged = self.get_asset_address( asset_name, to_chain, asset_origin_chain=from_chain) balance = aergo_u.get_balance(receiver, token_pegged, aergo_to) logger.info( "\U0001f4b0 %s balance on destination before transfer: %s", asset_name, balance / 10**18) except KeyError: logger.info("Pegged token unknow by wallet") save_pegged_token_address = True gas_limit = 300000 aer_balance = aergo_u.get_balance(tx_sender, 'aergo', aergo_to) if aer_balance < gas_limit * self.aergo_gas_price: err = "not enough aer balance to pay tx fee" raise InsufficientBalanceError(err) lock_proof = eth_to_aergo.build_lock_proof(w3, aergo_to, receiver, bridge_from, bridge_to, lock_height, asset_address) logger.info("\u2699 Built lock proof") token_pegged, tx_hash, _ = eth_to_aergo.mint(aergo_to, receiver, lock_proof, asset_address, bridge_to, gas_limit, self.aergo_gas_price) logger.info('\u26cf Mint success: %s', tx_hash) # new balance on destination balance = aergo_u.get_balance(receiver, token_pegged, aergo_to) logger.info("\U0001f4b0 %s balance on destination after transfer: %s", asset_name, balance / 10**18) aergo_to.disconnect() # record mint address in file if save_pegged_token_address: logger.info("------ Store mint address in config.json -----------") self.config_data('networks', from_chain, 'tokens', asset_name, 'pegs', to_chain, value=token_pegged) self.save_config() return tx_hash
def bridge_withdrawable_balance( account_addr: str, asset_address_origin: str, bridge_from: str, bridge_to: str, aergo_from: herapy.Aergo, aergo_to: herapy.Aergo, deposit_key: str, withdraw_key: str, ) -> Tuple[int, int]: account_ref = account_addr + asset_address_origin # total_deposit : total latest deposit including pending _, block_height = aergo_from.get_blockchain_status() block_from = aergo_from.get_block_headers(block_height=block_height, list_size=1) root_from = block_from[0].blocks_root_hash deposit_proof = aergo_from.query_sc_state(bridge_from, [deposit_key + account_ref], root=root_from, compressed=False) if not deposit_proof.account.state_proof.inclusion: raise InvalidArgumentsError( "Contract doesnt exist in state, check contract deployed and " "chain synced {}".format(deposit_proof)) total_deposit = 0 if deposit_proof.var_proofs[0].inclusion: total_deposit = int( deposit_proof.var_proofs[0].value.decode('utf-8')[1:-1]) # get total withdrawn and last anchor height withdraw_proof = aergo_to.query_sc_state( bridge_to, ["_sv__anchorHeight", withdraw_key + account_ref], compressed=False) if not withdraw_proof.account.state_proof.inclusion: raise InvalidArgumentsError( "Contract doesnt exist in state, check contract deployed and " "chain synced {}".format(withdraw_proof)) if not withdraw_proof.var_proofs[0].inclusion: raise InvalidArgumentsError("Cannot query last anchored height", withdraw_proof) total_withdrawn = 0 if withdraw_proof.var_proofs[1].inclusion: total_withdrawn = int( withdraw_proof.var_proofs[1].value.decode('utf-8')[1:-1]) last_anchor_height = int(withdraw_proof.var_proofs[0].value) # get anchored deposit : total deposit before the last anchor block_from = aergo_from.get_block_headers(block_height=last_anchor_height, list_size=1) root_from = block_from[0].blocks_root_hash deposit_proof = aergo_from.query_sc_state(bridge_from, [deposit_key + account_ref], root=root_from, compressed=False) if not deposit_proof.account.state_proof.inclusion: raise InvalidArgumentsError( "Contract doesnt exist in state, check contract deployed and " "chain synced {}".format(deposit_proof)) anchored_deposit = 0 if deposit_proof.var_proofs[0].inclusion: anchored_deposit = int( deposit_proof.var_proofs[0].value.decode('utf-8')[1:-1]) withdrawable_balance = anchored_deposit - total_withdrawn pending = total_deposit - anchored_deposit return withdrawable_balance, pending
def build_deposit_proof( aergo_from: herapy.Aergo, aergo_to: herapy.Aergo, receiver: str, bridge_from: str, bridge_to: str, deposit_height: int, token_origin: str, key_word: str ) -> herapy.obj.sc_state.SCState: """ Check the last anchored root includes the lock and build a lock proof for that root """ if not is_aergo_address(receiver): raise InvalidArgumentsError( "Receiver {} must be an Aergo address".format(receiver) ) # check last merged height anchor_info = aergo_to.query_sc_state(bridge_to, ["_sv__anchorHeight"]) if not anchor_info.account.state_proof.inclusion: raise InvalidArgumentsError( "Contract doesnt exist in state, check contract deployed and " "chain synced {}".format(anchor_info)) if not anchor_info.var_proofs[0].inclusion: raise InvalidArgumentsError("Cannot query last anchored height", anchor_info) last_merged_height_to = int(anchor_info.var_proofs[0].value) _, current_height = aergo_to.get_blockchain_status() # waite for anchor containing our transfer stream = aergo_to.receive_event_stream(bridge_to, "newAnchor", start_block_no=current_height) while last_merged_height_to < deposit_height: logger.info( "deposit not recorded in current anchor, waiting new anchor " "event... / deposit height : %s / last anchor height : %s ", deposit_height, last_merged_height_to ) new_anchor_event = next(stream) last_merged_height_to = new_anchor_event.arguments[1] stream.stop() # get inclusion proof of lock in last merged block merge_block_from = aergo_from.get_block_headers( block_height=last_merged_height_to, list_size=1) root_from = merge_block_from[0].blocks_root_hash account_ref = receiver + token_origin proof = aergo_from.query_sc_state( bridge_from, [key_word + account_ref], root=root_from, compressed=False ) if not proof.verify_proof(root_from): raise InvalidMerkleProofError("Unable to verify {} proof" .format(key_word)) if not proof.account.state_proof.inclusion: raise InvalidMerkleProofError( "Contract doesnt exist in state, check contract deployed and " "chain synced {}".format(proof)) if not proof.var_proofs[0].inclusion: raise InvalidMerkleProofError( "No tokens deposited for this account reference: {}" .format(proof)) return proof