def build_burn_proof( aergo_from: herapy.Aergo, w3: Web3, receiver: str, bridge_from: str, bridge_to: str, bridge_to_abi: str, burn_height: int, token_origin: str, ): """ Check the last anchored root includes the burn and build a burn proof for that root """ if not is_ethereum_address(receiver): raise InvalidArgumentsError( "Receiver {} must be an Ethereum address".format(receiver) ) if not is_ethereum_address(token_origin): raise InvalidArgumentsError( "token_origin {} must be an Ethereum address".format(token_origin) ) trie_key = "_sv__burns-".encode('utf-8') + bytes.fromhex(receiver[2:]) \ + bytes.fromhex(token_origin[2:]) return _build_deposit_proof( aergo_from, w3, bridge_from, bridge_to, bridge_to_abi, burn_height, trie_key )
def unlockable_to_eth( self, from_chain: str, to_chain: str, asset_name: str, receiver: str, ) -> Tuple[int, int]: """Check unlockable balance on Ethereum.""" token_origin = self.get_asset_address(asset_name, to_chain) bridge_from = self.get_bridge_contract_address(from_chain, to_chain) bridge_to = self.get_bridge_contract_address(to_chain, from_chain) if not is_ethereum_address(token_origin): raise InvalidArgumentsError( "token_origin {} must be an Ethereum address".format( token_origin)) if not is_ethereum_address(receiver): raise InvalidArgumentsError( "Receiver {} must be an Ethereum address".format(receiver)) hera = self.connect_aergo(from_chain) w3 = self.get_web3(to_chain) account_ref = receiver[2:] + token_origin[2:] position = b'\x06' # Unlocks eth_trie_key = keccak( bytes.fromhex(account_ref) + position.rjust(32, b'\0')) aergo_storage_key = '_sv__burns-'.encode('utf-8') \ + bytes.fromhex(account_ref) return aergo_to_eth.withdrawable(bridge_from, bridge_to, hera, w3, aergo_storage_key, eth_trie_key)
def unlock( w3: Web3, signer_acct, receiver: str, burn_proof: herapy.obj.sc_state.SCState, token_origin: str, bridge_to: str, bridge_to_abi: str, gas_limit: int, gas_price: int ) -> Tuple[str, AttributeDict]: """ Unlock the receiver's burnt balance on aergo_to. """ if not is_ethereum_address(receiver): raise InvalidArgumentsError( "Receiver {} must be an Ethereum address".format(receiver) ) if not is_ethereum_address(token_origin): raise InvalidArgumentsError( "token_origin {} must be an Ethereum address".format(token_origin) ) receiver = Web3.toChecksumAddress(receiver) balance = int(burn_proof.var_proofs[0].value.decode('utf-8')[1:-1]) ap = burn_proof.var_proofs[0].auditPath bitmap = burn_proof.var_proofs[0].bitmap leaf_height = burn_proof.var_proofs[0].height # call mint on ethereum with the lock proof from aergo_from eth_bridge = w3.eth.contract( address=bridge_to, abi=bridge_to_abi ) construct_txn = eth_bridge.functions.unlock( receiver, balance, token_origin, ap, bitmap, leaf_height ).buildTransaction({ 'chainId': w3.eth.chainId, 'from': signer_acct.address, 'nonce': w3.eth.getTransactionCount( signer_acct.address ), '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("Unlock asset Tx execution failed: {}".format(receipt)) logger.info("\u26fd Eth Gas used: %s", receipt.gasUsed) return tx_hash.hex(), receipt
def mint(aergo_to: herapy.Aergo, receiver: str, lock_proof: AttributeDict, token_origin: str, bridge_to: str, gas_limit: int, gas_price: int) -> Tuple[str, 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_ethereum_address(token_origin): raise InvalidArgumentsError( "token_origin {} must be an Ethereum address".format(token_origin)) ap = format_proof_for_lua(lock_proof.storageProof[0].proof) balance = int.from_bytes(lock_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, "mint", args=[receiver, ubig_balance, token_origin[2:].lower(), ap], gas_limit=gas_limit, gas_price=gas_price) if result.status != herapy.CommitStatus.TX_OK: raise TxError("Mint asset Tx commit failed : {}".format(result)) result = aergo_to.wait_tx_result(tx.tx_hash) if result.status != herapy.TxResultStatus.SUCCESS: raise TxError("Mint asset Tx execution failed : {}".format(result)) logger.info("\u26fd Aergo gas used: %s", result.gas_used) token_pegged = json.loads(result.detail)[0] return token_pegged, str(tx.tx_hash), result
def unfreezable( self, from_chain: str, to_chain: str, receiver: str, ) -> Tuple[int, int]: """Check unfreezable balance on Aergo.""" token_origin = self.get_asset_address('aergo_erc20', from_chain) bridge_from = self.get_bridge_contract_address(from_chain, to_chain) bridge_to = self.get_bridge_contract_address(to_chain, from_chain) if not is_ethereum_address(token_origin): raise InvalidArgumentsError( "token_origin {} must be an Ethereum address".format( token_origin)) if not is_aergo_address(receiver): raise InvalidArgumentsError( "Receiver {} must be an Aergo address".format(receiver)) hera = self.connect_aergo(to_chain) w3 = self.get_web3(from_chain) account_ref_eth = \ receiver.encode('utf-8') + bytes.fromhex(token_origin[2:]) position = b'\x05' # Locks eth_trie_key = keccak(account_ref_eth + position.rjust(32, b'\0')) aergo_storage_key = ('_sv__unfreezes-' + receiver).encode('utf-8') \ + bytes.fromhex(token_origin[2:]) return eth_to_aergo.withdrawable(bridge_from, bridge_to, w3, hera, eth_trie_key, aergo_storage_key)
def burn(w3: Web3, signer_acct, receiver: str, amount: int, bridge_from: str, bridge_from_abi: str, token_pegged: str, gas_limit: int, gas_price: int) -> Tuple[int, str, AttributeDict]: """ Burn a token that was minted on ethereum. """ if not is_aergo_address(receiver): raise InvalidArgumentsError( "Receiver {} must be an Aergo address".format(receiver)) if not is_ethereum_address(token_pegged): raise InvalidArgumentsError( "token_pegged {} must be an Ethereum address".format(token_pegged)) bridge_from = Web3.toChecksumAddress(bridge_from) eth_bridge = w3.eth.contract(address=bridge_from, abi=bridge_from_abi) construct_txn = eth_bridge.functions.burn( receiver, amount, token_pegged).buildTransaction({ 'chainId': w3.eth.chainId, 'from': signer_acct.address, 'nonce': w3.eth.getTransactionCount(signer_acct.address), '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("Burn asset Tx execution failed: {}".format(receipt)) logger.info("\u26fd Eth Gas used: %s", receipt.gasUsed) return receipt.blockNumber, tx_hash.hex(), receipt
def get_balance_eth( 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.get_eth_wallet_address(account_name) if not is_ethereum_address(account_addr): raise InvalidArgumentsError( "Account {} must be an Ethereum address".format(account_addr)) w3 = self.get_web3(network_name) asset_addr = self.get_asset_address(asset_name, network_name, asset_origin_chain) if asset_origin_chain is None: if asset_addr == 'ether': balance = eth_u.get_balance(account_addr, asset_addr, w3) return balance, asset_addr abi = self.load_erc20_abi(network_name, asset_name) else: abi = self.load_minted_erc20_abi(network_name, asset_origin_chain) balance = eth_u.get_balance(account_addr, asset_addr, w3, abi) return balance, asset_addr
def freeze( aergo_from: herapy.Aergo, bridge_from: str, receiver: str, value: int, gas_limit: int, gas_price: int, ) -> Tuple[int, str, Transaction]: """ Freeze aergo native """ if not is_ethereum_address(receiver): raise InvalidArgumentsError( "Receiver {} must be an Ethereum address".format(receiver)) args = (receiver[2:].lower(), {"_bignum": str(value)}) tx, result = aergo_from.call_sc(bridge_from, "freeze", amount=value, args=args, gas_limit=gas_limit, gas_price=gas_price) if result.status != herapy.CommitStatus.TX_OK: raise TxError("Freeze asset Tx commit failed : {}".format(result)) # Check freeze success result = aergo_from.wait_tx_result(tx.tx_hash) if result.status != herapy.TxResultStatus.SUCCESS: raise TxError("Freeze Aer Tx execution failed : {}".format(result)) logger.info("\u26fd Aergo gas used: %s", result.gas_used) # get precise burn height tx_detail = aergo_from.get_tx(tx.tx_hash) freeze_height = tx_detail.block.height return freeze_height, str(tx.tx_hash), tx_detail
def burn( aergo_from: herapy.Aergo, bridge_from: str, receiver: str, value: int, token_pegged: str, gas_limit: int, gas_price: int, ) -> Tuple[int, str, Transaction]: """ Burn a minted token on a sidechain. """ if not is_ethereum_address(receiver): raise InvalidArgumentsError( "Receiver {} must be an Ethereum address".format(receiver)) if not is_aergo_address(token_pegged): raise InvalidArgumentsError( "token_pegged {} must be an Aergo address".format(token_pegged)) args = (receiver[2:].lower(), {"_bignum": str(value)}, token_pegged) tx, result = aergo_from.call_sc(bridge_from, "burn", args=args, gas_limit=gas_limit, gas_price=gas_price) if result.status != herapy.CommitStatus.TX_OK: raise TxError("Burn asset Tx commit failed : {}".format(result)) # Check burn success result = aergo_from.wait_tx_result(tx.tx_hash) if result.status != herapy.TxResultStatus.SUCCESS: raise TxError("Burn asset Tx execution failed : {}".format(result)) logger.info("\u26fd Aergo gas used: %s", result.gas_used) # get precise burn height tx_detail = aergo_from.get_tx(tx.tx_hash) burn_height = tx_detail.block.height return burn_height, str(tx.tx_hash), tx_detail
def unlock_to_eth( self, from_chain: str, to_chain: str, asset_name: str, receiver: str = None, burn_height: int = 0, privkey_name: str = 'default', privkey_pwd: str = None, ) -> str: """ Finalize ERC20 or Eth transfer back to Ethereum origin """ logger.info(from_chain + ' -> ' + to_chain) bridge_to_abi = self.load_bridge_abi(to_chain, from_chain) erc20_abi = self.load_erc20_abi(to_chain, asset_name) aergo_from = self.connect_aergo(from_chain) # get ethereum tx signer 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, to_chain) balance = eth_u.get_balance(receiver, asset_address, w3, erc20_abi) logger.info( "\U0001f4b0 %s balance on destination before transfer : %s", asset_name, balance / 10**18) gas_limit = 200000 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) burn_proof = aergo_to_eth.build_burn_proof(aergo_from, w3, receiver, bridge_from, bridge_to, bridge_to_abi, burn_height, asset_address) logger.info("\u2699 Built burn proof") tx_hash, _ = aergo_to_eth.unlock(w3, signer_acct, receiver, burn_proof, asset_address, bridge_to, bridge_to_abi, gas_limit, self.eth_gas_price) logger.info('\U0001f513 Unlock success: %s', tx_hash) # new balance on origin balance = eth_u.get_balance(receiver, asset_address, w3, erc20_abi) logger.info("\U0001f4b0 %s balance on destination after transfer : %s", asset_name, balance / 10**18) aergo_from.disconnect() return tx_hash
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 lock_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 Aergo Standard Token transfer to Ethereum sidechain""" logger.info(from_chain + ' -> ' + to_chain) if not is_ethereum_address(receiver): raise InvalidArgumentsError( "receiver {} must be an Ethereum address".format(receiver)) if asset_name == 'aergo': raise InvalidArgumentsError( 'aer cannot be locked on Aergo, must be frozen') 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) asset_address = self.get_asset_address(asset_name, from_chain) gas_limit = 300000 balance = aergo_u.get_balance(sender, asset_address, 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) lock_height, tx_hash, _ = aergo_to_eth.lock(aergo_from, bridge_from, receiver, amount, asset_address, gas_limit, self.aergo_gas_price) logger.info('\U0001f512 Lock success: %s', tx_hash) # remaining balance on origin : aer or asset balance = aergo_u.get_balance(sender, asset_address, aergo_from) logger.info( "\U0001f4b0 remaining %s balance on origin after transfer: %s", asset_name, balance / 10**18) aergo_from.disconnect() return lock_height, tx_hash
def get_balance(account_addr: str, asset_addr: str, w3: Web3, erc20_abi: str = None) -> int: if not is_ethereum_address(account_addr): raise InvalidArgumentsError( "Account {} must be an Ethereum address".format(account_addr)) account_addr = Web3.toChecksumAddress(account_addr) balance = 0 if asset_addr == "ether": balance = w3.eth.getBalance(account_addr) else: asset_addr = Web3.toChecksumAddress(asset_addr) if erc20_abi is None: raise InvalidArgumentsError("Provide token abi to query balance") token_contract = w3.eth.contract(address=asset_addr, abi=erc20_abi) try: balance = token_contract.functions.balanceOf(account_addr).call() except BadFunctionCallOutput as e: raise InvalidArgumentsError(e, asset_addr) return balance
def lock( aergo_from: herapy.Aergo, bridge_from: str, receiver: str, value: int, asset: str, gas_limit: int, gas_price: int, ) -> Tuple[int, str, Transaction]: """ Lock can be called to lock aer or tokens. it supports delegated transfers when tx broadcaster is not the same as the token owner """ if not is_ethereum_address(receiver): raise InvalidArgumentsError( "receiver {} must be an Ethereum address".format(receiver)) if not is_aergo_address(asset): raise InvalidArgumentsError( "asset {} must be an Aergo address".format(asset)) args = (bridge_from, {"_bignum": str(value)}, receiver[2:].lower()) tx, result = aergo_from.call_sc(asset, "transfer", args=args, amount=0, gas_limit=gas_limit, gas_price=gas_price) if result.status != herapy.CommitStatus.TX_OK: raise TxError("Lock asset Tx commit failed : {}".format(result)) # Check lock success result = aergo_from.wait_tx_result(tx.tx_hash) if result.status != herapy.TxResultStatus.SUCCESS: raise TxError("Lock asset Tx execution failed : {}".format(result)) logger.info("\u26fd Aergo gas used: %s", result.gas_used) # get precise lock height tx_detail = aergo_from.get_tx(tx.tx_hash) lock_height = tx_detail.block.height return lock_height, str(tx.tx_hash), tx_detail
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 build_lock_proof( w3: Web3, aergo_to: herapy.Aergo, receiver: str, bridge_from: str, bridge_to: str, lock_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_ethereum_address(token_origin): raise InvalidArgumentsError( "token_origin {} must be an Ethereum address".format(token_origin)) account_ref = receiver.encode('utf-8') + bytes.fromhex(token_origin[2:]) # 'Locks is the 6th state var defined in solitity contract position = b'\x05' trie_key = keccak(account_ref + position.rjust(32, b'\0')) return _build_deposit_proof(w3, aergo_to, bridge_from, bridge_to, lock_height, trie_key)
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