def test_double_deposit_same_tx(dbsession, eth_network_id, eth_service, eth_asset_id): """Check that we have some logic to avoid depositing the same asset twice.""" # Create ETH holding account under an address with transaction.manager: # First create the address which holds our account network = dbsession.query(AssetNetwork).get(eth_network_id) address = CryptoAddress(network=network, address=eth_address_to_bin(TEST_ADDRESS)) dbsession.add(address) dbsession.flush() # Create deposit op with transaction.manager: address = dbsession.query(CryptoAddress).one_or_none() asset = dbsession.query(Asset).get(eth_asset_id) txid = txid_to_bin(TEST_TXID) op = address.deposit(Decimal(10), asset, txid, bin_to_txid(txid)) dbsession.add(op) dbsession.flush() op_id = op.id with transaction.manager: address = dbsession.query(CryptoAddress).one_or_none() asset = dbsession.query(Asset).get(eth_asset_id) txid = txid_to_bin(TEST_TXID) op = address.deposit(Decimal(10), asset, txid, bin_to_txid(txid)) assert op.id == op_id assert dbsession.query(CryptoOperation).count() == 1
def close_withdraw(): # Fill in details. # Block number will be filled in later, when confirmation updater picks a transaction receipt for this operation. op = dbsession.query(CryptoOperation).get(opid) op.txid = txid_to_bin(txid) op.block = None op.mark_broadcasted()
def test_event_claim_fees(web3, topped_up_hosted_wallet, coinbase): """We correctly can claim transaction fees from the hosted wallet contract.""" hosted_wallet = topped_up_hosted_wallet coinbase_address = coinbase # Do a withdraw to cause some fees listener, events = create_contract_listener(hosted_wallet.contract) assert hosted_wallet.get_balance() > TEST_VALUE txid = hosted_wallet.withdraw(coinbase_address, TEST_VALUE) confirm_transaction(web3, txid) # Claim fees for the withdraw operation claim_txid, price = hosted_wallet.claim_fees(txid) confirm_transaction(web3, claim_txid) # We should have event for withdraw + claims update_count = listener.poll() assert update_count == 2 assert len(events) == 2 event_name, input_data = events[-1] # Fee claim event assert event_name == "ClaimFee" assert input_data["txid"] == txid_to_bin( txid) # This was correctly targeted to original withdraw assert input_data["value"] == to_wei(price) # We claimed correct amount
def handle_event(self, event_name: str, contract_address: str, log_data: dict, log_entry: dict): """Map incoming EVM log to database entry.""" opid = self.get_unique_transaction_id(log_entry) existing_op = self.get_existing_op(opid, CryptoOperationType.deposit) if existing_op: # Already in the database, all we need to do is to call blocknumber updater now return False network = self.dbsession.query(AssetNetwork).get(self.network_id) address = self.dbsession.query(CryptoAddress).filter_by( address=eth_address_to_bin(contract_address), network=network).one() op = self.create_op(event_name, address, opid, log_data, log_entry) if not op: # This was an event we don't care about return False op.opid = opid op.txid = txid_to_bin(log_entry["transactionHash"]) op.block = int(log_entry["blockNumber"], 16) op.required_confirmation_count = self.confirmation_count self.dbsession.add(op) self.notify_deposit(op) return True
def perform_tx(): op = dbsession.query(CryptoOperation).get(opid) # Check everyting looks sane assert op.crypto_account.id assert op.crypto_account.account.id asset = op.holding_account.asset assert asset.id # Set information on asset that we have now created and have its smart contract id assert not asset.external_id, "Asset has been already assigned its smart contract id. Recreate error?" address = bin_to_eth_address(op.crypto_account.address.address) # Create Tonex proxy object token = Token.create_token(web3, name=asset.name, symbol=asset.symbol, supply=asset.supply, owner=address) # Call geth RPC API over Populus contract proxy op.txid = txid_to_bin(token.initial_txid) op.block = None op.external_address = eth_address_to_bin(token.address) asset.external_id = op.external_address op.mark_performed() op.mark_broadcasted()
def handle_event(self, event_name: str, contract_address: str, log_data: dict, log_entry: dict): """Map incoming EVM log to database entry.""" with self._get_tm(): opid = self.get_unique_transaction_id(log_entry) existing_op = self.get_existing_op(opid, CryptoOperationType.deposit) if existing_op: # Already in the database, all we need to do is to call blocknumber updater now return False network = self.dbsession.query(AssetNetwork).get(self.network_id) address = self.dbsession.query(CryptoAddress).filter_by(address=eth_address_to_bin(contract_address), network=network).one() op = self.create_op(event_name, address, opid, log_data, log_entry) if not op: # This was an event we don't care about return False op.opid = opid op.txid = txid_to_bin(log_entry["transactionHash"]) op.block = int(log_entry["blockNumber"], 16) op.required_confirmation_count = self.confirmation_count self.dbsession.add(op) self.notify_deposit(op) return True
def update_tx(self, current_block: int, txinfo: dict, receipt: dict) -> Tuple[int, int]: """Process logs from initial log run or filter updates. :return: (performed updates, failed updates) """ # We may have multiple ops for one transaction ops = self.dbsession.query(CryptoOperation).filter_by(txid=txid_to_bin(receipt["transactionHash"])) updates = failures = 0 for op in ops: # http://ethereum.stackexchange.com/q/6007/620 if txinfo["gas"] == receipt["gasUsed"]: op.other_info["failure_reason"] = "Smart contract rejected the transaction" op.mark_failure() failures += 1 # Withdraw operation has not gets it block yet # Block number may change because of the works assert receipt["blockNumber"].startswith("0x") op.block = int(receipt["blockNumber"], 16) confirmation_count = current_block - op.block if op.update_confirmations(confirmation_count): # Notify listeners we reached the goal logger.info("Completed, confirmations reached %s", op) self.registry.notify(CryptoOperationCompleted(op, self.registry, self.web3)) updates += 1 return updates, failures
def handle_event(self, event_name: str, contract_address: str, log_data: dict, log_entry: dict): """Map incoming EVM log to database entry.""" opid = self.get_unique_transaction_id(log_entry) existing_op = self.get_existing_op(opid, CryptoOperationType.deposit) if existing_op: # Already in the database, all we need to do is to call blocknumber updater now return False network = self.dbsession.query(AssetNetwork).get(self.network_id) asset = self.dbsession.query(Asset).filter_by( network=network, external_id=eth_address_to_bin(contract_address)).one() if event_name == "Transfer": to_address = eth_address_to_bin(log_data["to"]) from_address = eth_address_to_bin(log_data["from"]) value = Decimal(log_data["value"]) self.logger.debug("Incoming transfer event %s %s %s", from_address, to_address, value) # Get destination address entry address = self.dbsession.query(CryptoAddress).filter_by( address=to_address).one_or_none() if not address: # Address not in our system return False # Create operation op = CryptoAddressDeposit(network=network) op.opid = opid op.txid = txid_to_bin(log_entry["transactionHash"]) op.external_address = from_address op.block = int(log_entry["blockNumber"], 16) op.required_confirmation_count = self.confirmation_count op.crypto_account = address.get_or_create_account(asset) # Create holding account that keeps the value until we receive N amount of confirmations acc = Account(asset=asset) self.dbsession.add(acc) self.dbsession.flush() acc.do_withdraw_or_deposit( value, "Token {} deposit from {} in tx {}".format( asset.symbol, log_data["from"], log_entry["transactionHash"])) op.holding_account = acc self.dbsession.add(op) self.notify_deposit(op) return True else: # Unmonitored event return False
def get_unique_transaction_id(self, log_entry: dict) -> bytes: """Get txid - logindex pair. Because ethereum transactions may contain several log events due to cross contract calls, it's not enough to identify events by their transaction hash. Instead, we use tranaction hash - log index pair.""" txid = txid_to_bin(log_entry["transactionHash"]) logIndex = int(log_entry["logIndex"], 16) assert logIndex < 256 data = txid + bytes([logIndex]) assert len(data) < 34 return data
def handle_event(self, event_name: str, contract_address: str, log_data: dict, log_entry: dict): """Map incoming EVM log to database entry.""" with self._get_tm(): opid = self.get_unique_transaction_id(log_entry) existing_op = self.get_existing_op(opid, CryptoOperationType.deposit) if existing_op: # Already in the database, all we need to do is to call blocknumber updater now return False network = self.dbsession.query(AssetNetwork).get(self.network_id) asset = self.dbsession.query(Asset).filter_by(network=network, external_id=eth_address_to_bin(contract_address)).one() if event_name == "Transfer": to_address = eth_address_to_bin(log_data["to"]) from_address = eth_address_to_bin(log_data["from"]) value = Decimal(log_data["value"]) # Get destination address entry address = self.dbsession.query(CryptoAddress).filter_by(address=to_address).one_or_none() if not address: # Address not in our system return False # Create operation op = CryptoAddressDeposit(network=network) op.opid = opid op.txid = txid_to_bin(log_entry["transactionHash"]) op.external_address = from_address op.block = int(log_entry["blockNumber"], 16) op.required_confirmation_count = self.confirmation_count op.crypto_account = address.get_or_create_account(asset) # Create holding account that keeps the value until we receive N amount of confirmations acc = Account(asset=asset) self.dbsession.add(acc) self.dbsession.flush() acc.do_withdraw_or_deposit(value, "Token {} deposit from {} in tx {}".format(asset.symbol, log_data["from"], log_entry["transactionHash"])) op.holding_account = acc self.dbsession.add(op) self.notify_deposit(op) return True else: # Unmonitored event return False
def finish_op(): op = dbsession.query(CryptoOperation).get(opid) txid = wallet.initial_txid receipt = web3.eth.getTransactionReceipt(txid) op.txid = txid_to_bin(txid) op.block = receipt["blockNumber"] op.address.address = eth_address_to_bin(wallet.address) op.external_address = op.address.address # There is no broadcast wait, so we can close this right away op.mark_performed() op.mark_broadcasted() op.mark_complete()
def do_faux_deposit(address: CryptoAddress, asset_id, amount) -> CryptoAddressDeposit: """Simulate deposit to address.""" network = address.network txid = network.other_data["test_txid_pool"].pop() # txid = "0x00df829c5a142f1fccd7d8216c5785ac562ff41e2dcfdf5785ac562ff41e2dcf" dbsession = Session.object_session(address) asset = dbsession.query(Asset).get(asset_id) txid = txid_to_bin(txid) op = address.deposit(Decimal(amount), asset, txid, bin_to_txid(txid)) op.required_confirmation_count = 1 op.external_address = address.address dbsession.add(op) dbsession.flush() return op
def _create_address(web3, dbsession, opid): with transaction.manager: op = dbsession.query(CryptoOperation).get(opid) txid = TEST_ADDRESS_INITIAL_TXID # Deterministically pull faux addresses from pool network = op.network address_pool = network.other_data["test_address_pool"] address = address_pool.pop() op.txid = txid_to_bin(txid) op.block = 666 op.address.address = eth_address_to_bin(address) op.external_address = op.address.address op.mark_performed() op.mark_broadcasted()
def test_deposit_eth_account(dbsession, eth_network_id, eth_service, eth_asset_id): """Deposit Ethereums to an account.""" # Create ETH holding account under an address with transaction.manager: # First create the address which holds our account network = dbsession.query(AssetNetwork).get(eth_network_id) address = CryptoAddress(network=network, address=eth_address_to_bin(TEST_ADDRESS)) dbsession.add(address) dbsession.flush() # Create deposit op with transaction.manager: address = dbsession.query(CryptoAddress).one_or_none() asset = dbsession.query(Asset).get(eth_asset_id) txid = txid_to_bin(TEST_TXID) op = address.deposit(Decimal(10), asset, txid, bin_to_txid(txid)) dbsession.add(op) opid = op.id # Resolve deposit op success_op_count, failed_op_count = eth_service.run_waiting_operations() assert success_op_count == 1 assert failed_op_count == 0 with transaction.manager: op = dbsession.query(CryptoOperation).get(opid) op.resolve() # Force resolution regardless on confirmation count # Check balances are settled with transaction.manager: address = dbsession.query(CryptoAddress).one_or_none() asset = dbsession.query(Asset).get(eth_asset_id) account = address.get_account(asset) op = dbsession.query(CryptoOperation).one() assert account.account.get_balance() == Decimal(10) assert op.holding_account.get_balance() == 0 # Transaction label should be the Ethereum txid tx = account.account.transactions.one() assert tx.message == TEST_TXID
def _withdraw(web3, dbsession: Session, opid: UUID): with transaction.manager: # Check everyting looks sane op = dbsession.query(CryptoOperation).get(opid) assert op.crypto_account.id assert op.crypto_account.account.id assert op.holding_account.id assert op.holding_account.get_balance() > 0 assert op.external_address assert op.required_confirmation_count # Should be set by the creator op.mark_performed() # Don't try to pick this op automatically again # Fill in details. # Block number will be filled in later, when confirmation updater picks a transaction receipt for this operation. op = dbsession.query(CryptoOperation).get(opid) op.txid = txid_to_bin(TEST_ADDRESS_INITIAL_TXID) op.block = None op.mark_broadcasted()
def update_tx(self, current_block: int, txinfo: dict, receipt: dict) -> Tuple[int, int]: """Process logs from initial log run or filter updates. :return: (performed updates, failed updates) """ # We may have multiple ops for one transaction ops = self.dbsession.query(CryptoOperation).filter_by(txid=txid_to_bin(receipt["transactionHash"])) updates = failures = 0 for op in ops: # http://ethereum.stackexchange.com/q/6007/620 if txinfo["gas"] == receipt["gasUsed"]: op.mark_failed("Smart contract rejected the transaction") failures += 1 continue failure_reason = self.check_bad_hosted_wallet_events(op, receipt) if failure_reason: op.mark_failed(failure_reason) failures += 1 continue # Withdraw operation has not gets it block yet # Block number may change because of the works assert receipt["blockNumber"].startswith("0x") op.block = int(receipt["blockNumber"], 16) confirmation_count = current_block - op.block if op.update_confirmations(confirmation_count): # Notify listeners we reached the goal logger.info("Completed, confirmations reached %s", op) self.registry.notify(CryptoOperationCompleted(op, self.registry, self.web3)) updates += 1 return updates, failures
def _create_token(web3, dbsession, opid): with transaction.manager: op = dbsession.query(CryptoOperation).get(opid) # Check everyting looks sane assert op.crypto_account.id assert op.crypto_account.account.id asset = op.holding_account.asset assert asset.id # Set information on asset that we have now created and have its smart contract id assert not asset.external_id, "Asset has been already assigned its smart contract id. Recreate error?" address = bin_to_eth_address(op.crypto_account.address.address) # Call geth RPC API over Populus contract proxy op.txid = txid_to_bin(TEST_TOKEN_TXID) op.block = None op.external_address = eth_address_to_bin(TEST_TOKEN_ADDRESS) asset.external_id = op.external_address op.mark_performed() op.mark_broadcasted()
def test_withdraw_eth_account(dbsession, eth_service, eth_network_id, eth_asset_id): """Withdraw ETHs to an address.""" # Create ETH holding account under an address with transaction.manager: # First create the address which holds our account network = dbsession.query(AssetNetwork).get(eth_network_id) address = CryptoAddress(network=network, address=eth_address_to_bin(TEST_ADDRESS)) dbsession.flush() assert address.id assert address.address asset = dbsession.query(Asset).get(eth_asset_id) # Create an account of ETH tokens on that address ca_account = address.create_account(asset) # It should have zero balance by default with transaction.manager: ca_account = dbsession.query(CryptoAddressAccount).one_or_none() assert ca_account.account.asset_id == eth_asset_id assert ca_account.account.get_balance() == Decimal(0) # Faux top up so we have value to withdraw with transaction.manager: ca_account = dbsession.query(CryptoAddressAccount).one_or_none() assert ca_account.account.do_withdraw_or_deposit( Decimal("+10"), "Faux top up") # Create withdraw operations withdraw_address = eth_address_to_bin(TEST_ADDRESS) with transaction.manager: ca_account = dbsession.query(CryptoAddressAccount).one_or_none() op = ca_account.withdraw(Decimal("10"), withdraw_address, "Bailing out") # We withdraw 10 ETHs assert op.holding_account.get_balance() == Decimal("10") assert op.holding_account.asset == dbsession.query(Asset).get( eth_asset_id) assert op.holding_account.transactions.count() == 1 assert op.holding_account.transactions.first().message == "Bailing out" # Check all looks good on sending account assert ca_account.account.transactions.count() == 2 assert ca_account.account.transactions.all( )[0].message == "Faux top up" assert ca_account.account.transactions.all( )[1].message == "Bailing out" assert ca_account.account.get_balance() == 0 def _withdraw_eth(service, dbsession, opid): # Mocked withdraw op that always success with transaction.manager: op = dbsession.query(CryptoOperation).get(opid) op.txid = txid_to_bin(TEST_TXID) op.mark_complete() with mock.patch("websauna.wallet.ethereum.ops.withdraw_eth", new=_withdraw_eth): success_op_count, failed_op_count = eth_service.run_waiting_operations( ) # Check that operations have been marked as success with transaction.manager: ops = list(dbsession.query(CryptoOperation).all()) assert len(ops) == 1 assert isinstance(ops[0], CryptoAddressWithdraw) assert ops[0].state == CryptoOperationState.success assert ops[0].txid == txid_to_bin(TEST_TXID)
def _withdraw_eth(service, dbsession, opid): # Mocked withdraw op that always success with transaction.manager: op = dbsession.query(CryptoOperation).get(opid) op.txid = txid_to_bin(TEST_TXID) op.mark_complete()
def test_withdraw_eth_account(dbsession, eth_service, eth_network_id, eth_asset_id): """Withdraw ETHs to an address.""" # Create ETH holding account under an address with transaction.manager: # First create the address which holds our account network = dbsession.query(AssetNetwork).get(eth_network_id) address = CryptoAddress(network=network, address=eth_address_to_bin(TEST_ADDRESS)) dbsession.flush() assert address.id assert address.address asset = dbsession.query(Asset).get(eth_asset_id) # Create an account of ETH tokens on that address ca_account = address.create_account(asset) # It should have zero balance by default with transaction.manager: ca_account = dbsession.query(CryptoAddressAccount).one_or_none() assert ca_account.account.asset_id == eth_asset_id assert ca_account.account.get_balance() == Decimal(0) # Faux top up so we have value to withdraw with transaction.manager: ca_account = dbsession.query(CryptoAddressAccount).one_or_none() assert ca_account.account.do_withdraw_or_deposit(Decimal("+10"), "Faux top up") # Create withdraw operations withdraw_address = eth_address_to_bin(TEST_ADDRESS) with transaction.manager: ca_account = dbsession.query(CryptoAddressAccount).one_or_none() op = ca_account.withdraw(Decimal("10"), withdraw_address, "Bailing out") # We withdraw 10 ETHs assert op.holding_account.get_balance() == Decimal("10") assert op.holding_account.asset == dbsession.query(Asset).get(eth_asset_id) assert op.holding_account.transactions.count() == 1 assert op.holding_account.transactions.first().message == "Bailing out" # Check all looks good on sending account assert ca_account.account.transactions.count() == 2 assert ca_account.account.transactions.all()[0].message == "Faux top up" assert ca_account.account.transactions.all()[1].message == "Bailing out" assert ca_account.account.get_balance() == 0 def _withdraw_eth(service, dbsession, opid): # Mocked withdraw op that always success with transaction.manager: op = dbsession.query(CryptoOperation).get(opid) op.txid = txid_to_bin(TEST_TXID) op.mark_complete() with mock.patch("websauna.wallet.ethereum.ops.withdraw_eth", new=_withdraw_eth): success_op_count, failed_op_count = eth_service.run_waiting_operations() # Check that operations have been marked as success with transaction.manager: ops = list(dbsession.query(CryptoOperation).all()) assert len(ops) == 1 assert isinstance(ops[0], CryptoAddressWithdraw) assert ops[0].state == CryptoOperationState.success assert ops[0].txid == txid_to_bin(TEST_TXID)
def test_transfer_tokens_between_accounts(dbsession, eth_network_id, web3: Web3, eth_service: EthereumService, deposit_address: str, token_asset: str, coinbase: str): """Transfer tokens between two internal accounts. Do transfers in two batches to make sure subsequent events top up correctly. """ # Initiate a target address creatin with transaction.manager: network = dbsession.query(AssetNetwork).get(eth_network_id) opid = CryptoAddress.create_address(network).id # Run address creation success_count, failure_count = eth_service.run_waiting_operations() assert success_count == 1 assert failure_count == 0 # We resolved address creation operation. # Get the fresh address for the future withdraw targets. with transaction.manager: op = dbsession.query(CryptoAddressCreation).get(opid) addr = op.address.address assert addr # Move tokens between accounts with transaction.manager: address = dbsession.query(CryptoAddress).filter_by(address=eth_address_to_bin(deposit_address)).one() asset = dbsession.query(Asset).get(token_asset) caccount = address.get_account(asset) op = caccount.withdraw(Decimal(2000), addr, "Sending to friend") opid = op.id # Resolve the 1st transaction eth_service.run_event_cycle() wait_for_op_confirmations(eth_service, opid) # See that our internal balances match with transaction.manager: # Withdraw operation is complete op = dbsession.query(CryptoOperation).get(opid) assert op.completed_at assert op.completed_at, "Op not confirmed {}".format(op) # We should have received a Transfer operation targetting target account op = dbsession.query(CryptoOperation).join(CryptoAddressAccount).join(CryptoAddress).filter_by(address=addr).one() opid = op.id # Confirm incoming Transfer wait_for_op_confirmations(eth_service, opid, timeout=180) # Check Transfer looks valid with transaction.manager: op = dbsession.query(CryptoOperation).get(opid) assert op.completed_at assert op.completed_at, "Op not confirmed {}".format(op) asset = dbsession.query(Asset).get(token_asset) source = dbsession.query(CryptoAddress).filter_by(address=eth_address_to_bin(deposit_address)).one() target = dbsession.query(CryptoAddress).filter_by(address=addr).one() assert source.get_account(asset).account.get_balance() == 8000 assert target.get_account(asset).account.get_balance() == 2000 # Add some ETH on deposit_address txid = send_balance_to_address(web3, deposit_address, TEST_VALUE) confirm_transaction(web3, txid) eth_service.run_event_cycle() # Wait for confirmations to have ETH deposit credired with transaction.manager: op = dbsession.query(CryptoOperation).filter_by(txid=txid_to_bin(txid)).one() opid = op.id confirmed = op.completed_at if not confirmed: wait_for_op_confirmations(eth_service, opid) # Send some more tokens + ether, so we see account can't get mixed up with transaction.manager: address = dbsession.query(CryptoAddress).filter_by(address=eth_address_to_bin(deposit_address)).one() asset = dbsession.query(Asset).get(token_asset) caccount = address.get_account(asset) op = caccount.withdraw(Decimal(4000), addr, "Sending tokens to friend äää") eth_asset = get_ether_asset(dbsession) caccount = address.get_account(eth_asset) op = caccount.withdraw(TEST_VALUE, addr, "Sending ETH to friend äää") opid = op.id # Resolve second transaction eth_service.run_event_cycle() wait_for_op_confirmations(eth_service, opid) # See that our internal balances match with transaction.manager: op = dbsession.query(CryptoOperation).get(opid) assert op.completed_at assert op.completed_at asset = dbsession.query(Asset).get(token_asset) source = dbsession.query(CryptoAddress).filter_by(address=eth_address_to_bin(deposit_address)).one() target = dbsession.query(CryptoAddress).filter_by(address=addr).one() eth_asset = get_ether_asset(dbsession) assert source.get_account(asset).account.get_balance() == 4000 assert target.get_account(asset).account.get_balance() == 6000 assert target.get_account(eth_asset).account.get_balance() == TEST_VALUE # Transfer 3: # Send everything back plus some ether with transaction.manager: address = dbsession.query(CryptoAddress).filter_by(address=addr).one() asset = dbsession.query(Asset).get(token_asset) caccount = address.get_account(asset) op = caccount.withdraw(Decimal(6000), eth_address_to_bin(deposit_address), "Sending everything back to friend äää") opid = op.id # Resolve third transaction wait_for_op_confirmations(eth_service, opid) eth_service.run_event_cycle() # See that our internal balances match with transaction.manager: op = dbsession.query(CryptoOperation).get(opid) assert op.completed_at assert op.completed_at asset = dbsession.query(Asset).get(token_asset) source = dbsession.query(CryptoAddress).filter_by(address=eth_address_to_bin(deposit_address)).one() target = dbsession.query(CryptoAddress).filter_by(address=addr).one() assert source.get_account(asset).account.get_balance() == 10000 assert target.get_account(asset).account.get_balance() == 0 eth_asset = get_ether_asset(dbsession) assert source.get_account(eth_asset).account.get_balance() == 0 assert target.get_account(eth_asset).account.get_balance() == TEST_VALUE