def save(self, **kwargs): safe_address = self.context['safe_address'] ethereum_client = EthereumClientProvider() safe = Safe(safe_address, ethereum_client) safe_tx_gas = safe.estimate_tx_gas(self.validated_data['to'], self.validated_data['value'], self.validated_data['data'], self.validated_data['operation']) return {'safe_tx_gas': safe_tx_gas}
def validate(self, data): super().validate(data) if not SafeContract.objects.filter(address=data['safe']).exists(): raise ValidationError(f"Safe={data['safe']} does not exist or it's still not indexed") ethereum_client = EthereumClientProvider() safe = Safe(data['safe'], ethereum_client) # Check owners and pending owners try: safe_owners = safe.retrieve_owners(block_identifier='pending') except BadFunctionCallOutput: # Error using pending block identifier safe_owners = safe.retrieve_owners(block_identifier='latest') signature = data['signature'] delegate = data['delegate'] # Delegate address to be added # Tries to find a valid delegator using multiple strategies for operation_hash in (DelegateSignatureHelper.calculate_hash(delegate), DelegateSignatureHelper.calculate_hash(delegate, eth_sign=True), DelegateSignatureHelper.calculate_hash(delegate, previous_topt=True), DelegateSignatureHelper.calculate_hash(delegate, eth_sign=True, previous_topt=True)): delegator = self.check_signature(signature, operation_hash, safe_owners) if delegator: break if not delegator: raise ValidationError('Signing owner is not an owner of the Safe') data['delegator'] = delegator return data
def validate_signature(self, signature: bytes): safe_tx_hash = self.context['safe_tx_hash'] try: multisig_transaction = MultisigTransaction.objects.select_related( 'ethereum_tx' ).get(safe_tx_hash=safe_tx_hash) except MultisigTransaction.DoesNotExist: raise NotFound(f'Multisig transaction with safe-tx-hash={safe_tx_hash} was not found') safe_address = multisig_transaction.safe if multisig_transaction.executed: raise ValidationError(f'Transaction with safe-tx-hash={safe_tx_hash} was already executed') ethereum_client = EthereumClientProvider() safe = Safe(safe_address, ethereum_client) try: safe_owners = safe.retrieve_owners(block_identifier='pending') except BadFunctionCallOutput: # Error using pending block identifier safe_owners = safe.retrieve_owners(block_identifier='latest') parsed_signatures = SafeSignature.parse_signature(signature, safe_tx_hash) signature_owners = [] for safe_signature in parsed_signatures: owner = safe_signature.owner if owner not in safe_owners: raise ValidationError(f'Signer={owner} is not an owner. Current owners={safe_owners}') if not safe_signature.is_valid(ethereum_client, safe.address): raise ValidationError(f'Signature={safe_signature.signature.hex()} for owner={owner} is not valid') if owner in signature_owners: raise ValidationError(f'Signature for owner={owner} is duplicated') signature_owners.append(owner) return signature
def test_post_multisig_transactions(self): safe_owner_1 = Account.create() safe_create2_tx = self.deploy_test_safe(owners=[safe_owner_1.address]) safe_address = safe_create2_tx.safe_address safe = Safe(safe_address, self.ethereum_client) response = self.client.get(reverse('v1:multisig-transactions', args=(safe_address,)), format='json') self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) to = Account.create().address data = {"to": to, "value": 100000000000000000, "data": None, "operation": 0, "nonce": 0, "safeTxGas": 0, "baseGas": 0, "gasPrice": 0, "gasToken": "0x0000000000000000000000000000000000000000", "refundReceiver": "0x0000000000000000000000000000000000000000", # "contractTransactionHash": "0x1c2c77b29086701ccdda7836c399112a9b715c6a153f6c8f75c84da4297f60d3", "sender": safe_owner_1.address, } safe_tx = safe.build_multisig_tx(data['to'], data['value'], data['data'], data['operation'], data['safeTxGas'], data['baseGas'], data['gasPrice'], data['gasToken'], data['refundReceiver'], safe_nonce=data['nonce']) data['contractTransactionHash'] = safe_tx.safe_tx_hash.hex() response = self.client.post(reverse('v1:multisig-transactions', args=(safe_address,)), format='json', data=data) self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) response = self.client.get(reverse('v1:multisig-transactions', args=(safe_address,)), format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data['results']), 1) self.assertIsNone(response.data['results'][0]['executor']) self.assertEqual(len(response.data['results'][0]['confirmations']), 0) # Test confirmation with signature data['signature'] = safe_owner_1.signHash(safe_tx.safe_tx_hash)['signature'].hex() response = self.client.post(reverse('v1:multisig-transactions', args=(safe_address,)), format='json', data=data) self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) response = self.client.get(reverse('v1:multisig-transactions', args=(safe_address,)), format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data['results']), 1) self.assertEqual(len(response.data['results'][0]['confirmations']), 1) self.assertEqual(response.data['results'][0]['confirmations'][0]['signature'], data['signature']) # Sign with a different user that sender random_user_account = Account.create() data['signature'] = random_user_account.signHash(safe_tx.safe_tx_hash)['signature'].hex() response = self.client.post(reverse('v1:multisig-transactions', args=(safe_address,)), format='json', data=data) self.assertIn('Signature does not match sender', response.data['non_field_errors'][0]) self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) # Sign with a random user (not owner) data['sender'] = random_user_account.address response = self.client.post(reverse('v1:multisig-transactions', args=(safe_address,)), format='json', data=data) self.assertIn('User is not an owner', response.data['non_field_errors'][0]) self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY)
def deploy_test_safe_v1_0_0( self, number_owners: int = 3, threshold: Optional[int] = None, owners: Optional[List[ChecksumAddress]] = None, initial_funding_wei: int = 0, ) -> Safe: owners = (owners if owners else [Account.create().address for _ in range(number_owners)]) if not threshold: threshold = len(owners) - 1 if len(owners) > 1 else 1 empty_parameters = {"gas": 1, "gasPrice": 1} to = NULL_ADDRESS data = b"" payment_token = NULL_ADDRESS payment = 0 payment_receiver = NULL_ADDRESS initializer = HexBytes( self.safe_contract_V1_0_0.functions.setup( owners, threshold, to, data, payment_token, payment, payment_receiver).buildTransaction(empty_parameters)["data"]) ethereum_tx_sent = self.proxy_factory.deploy_proxy_contract( self.ethereum_test_account, self.safe_contract_V1_0_0_address, initializer=initializer, ) safe = Safe(ethereum_tx_sent.contract_address, self.ethereum_client) if initial_funding_wei: self.send_ether(safe.address, initial_funding_wei) self.assertEqual(safe.retrieve_version(), "1.0.0") self.assertEqual(safe.retrieve_threshold(), threshold) self.assertCountEqual(safe.retrieve_owners(), owners) return safe
def test_post_multisig_transactions_with_origin(self): safe_owner_1 = Account.create() safe_create2_tx = self.deploy_test_safe(owners=[safe_owner_1.address]) safe_address = safe_create2_tx.safe_address safe = Safe(safe_address, self.ethereum_client) response = self.client.get(reverse('v1:multisig-transactions', args=(safe_address,)), format='json') self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) to = Account.create().address data = {"to": to, "value": 100000000000000000, "data": None, "operation": 0, "nonce": 0, "safeTxGas": 0, "baseGas": 0, "gasPrice": 0, "gasToken": "0x0000000000000000000000000000000000000000", "refundReceiver": "0x0000000000000000000000000000000000000000", # "contractTransactionHash": "0x1c2c77b29086701ccdda7836c399112a9b715c6a153f6c8f75c84da4297f60d3", "sender": safe_owner_1.address, "origin": 'Testing origin field', } safe_tx = safe.build_multisig_tx(data['to'], data['value'], data['data'], data['operation'], data['safeTxGas'], data['baseGas'], data['gasPrice'], data['gasToken'], data['refundReceiver'], safe_nonce=data['nonce']) data['contractTransactionHash'] = safe_tx.safe_tx_hash.hex() response = self.client.post(reverse('v1:multisig-transactions', args=(safe_address,)), format='json', data=data) self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) multisig_tx_db = MultisigTransaction.objects.get(safe_tx_hash=safe_tx.safe_tx_hash) self.assertEqual(multisig_tx_db.origin, data['origin'])
def test_deploy_proxy_contract_with_nonce(self): salt_nonce = generate_salt_nonce() owners = [Account.create().address for _ in range(2)] threshold = 2 payment_token = None safe_create2_tx = Safe.build_safe_create2_tx( self.ethereum_client, self.safe_contract_address, self.proxy_factory_contract_address, salt_nonce, owners, threshold, self.gas_price, payment_token) # Send ether for safe deploying costs self.send_tx( { 'to': safe_create2_tx.safe_address, 'value': safe_create2_tx.payment }, self.ethereum_test_account) proxy_factory = ProxyFactory(self.proxy_factory_contract_address, self.ethereum_client) ethereum_tx_sent = proxy_factory.deploy_proxy_contract_with_nonce( self.ethereum_test_account, safe_create2_tx.master_copy_address, safe_create2_tx.safe_setup_data, salt_nonce, safe_create2_tx.gas, gas_price=self.gas_price) receipt = self.ethereum_client.get_transaction_receipt( ethereum_tx_sent.tx_hash, timeout=20) self.assertEqual(receipt.status, 1) safe = Safe(ethereum_tx_sent.contract_address, self.ethereum_client) self.assertEqual(ethereum_tx_sent.contract_address, safe_create2_tx.safe_address) self.assertEqual(set(safe.retrieve_owners()), set(owners)) self.assertEqual(safe.retrieve_master_copy_address(), safe_create2_tx.master_copy_address)
def test_change_fallback_handler(self): safe_operator = self.setup_operator() safe = Safe(safe_operator.address, self.ethereum_client) current_fallback_handler = safe.retrieve_fallback_handler() with self.assertRaises(SameFallbackHandlerException): safe_operator.change_fallback_handler(current_fallback_handler) new_fallback_handler = Account.create().address with self.assertRaises( InvalidFallbackHandlerException ): # Contract does not exist self.assertTrue(safe_operator.change_fallback_handler(new_fallback_handler)) with mock.patch.object( EthereumClient, "is_contract", autospec=True, return_value=True ): self.assertTrue(safe_operator.change_fallback_handler(new_fallback_handler)) self.assertEqual( safe_operator.safe_cli_info.fallback_handler, new_fallback_handler ) self.assertEqual(safe.retrieve_fallback_handler(), new_fallback_handler) safe_operator.change_master_copy(self.safe_old_contract_address) with self.assertRaises(FallbackHandlerNotSupportedException): safe_operator.change_fallback_handler(Account.create().address)
def get_safe_info(self, safe_address: str) -> SafeInfo: try: safe = Safe(safe_address, self.ethereum_client) return safe.retrieve_all_info() except IOError as exc: raise NodeConnectionError from exc except CannotRetrieveSafeInfoException as e: raise CannotGetSafeInfo from e
def get_safe_owners( self, ethereum_client: EthereumClient, safe_address: ChecksumAddress) -> List[ChecksumAddress]: safe = Safe(safe_address, ethereum_client) try: return safe.retrieve_owners(block_identifier="pending") except BadFunctionCallOutput: # Error using pending block identifier return safe.retrieve_owners(block_identifier="latest")
def retrieve_safe_info(self, address: str) -> SafeInfo: safe = Safe(address, self.ethereum_client) if not self.ethereum_client.is_contract(address): raise SafeNotDeployed('Safe with address=%s not deployed' % address) try: return safe.retrieve_all_info() except CannotRetrieveSafeInfoException as e: raise CannotRetrieveSafeInfo(address) from e
def get_safe_owners(safe_address: str) -> Sequence[str]: ethereum_client = EthereumClientProvider() safe = Safe(safe_address, ethereum_client) try: return safe.retrieve_owners(block_identifier="pending") except BadFunctionCallOutput: # Error using pending block identifier try: return safe.retrieve_owners(block_identifier="latest") except BadFunctionCallOutput: return []
def resend(self, gas_price: int, multisig_tx: SafeMultisigTx) -> Optional[EthereumTx]: """ Resend transaction with `gas_price` if it's higher or equal than transaction gas price. Setting equal `gas_price` is allowed as sometimes a transaction can be out of the mempool but `gas_price` does not need to be increased when resending :param gas_price: New gas price for the transaction. Must be >= old gas price :param multisig_tx: Multisig Tx not mined to be sent again :return: If a new transaction is sent is returned, `None` if not """ if multisig_tx.ethereum_tx.gas_price > gas_price: logger.info( '%s tx gas price is %d > %d. Nothing to do here', multisig_tx.ethereum_tx_id, multisig_tx.ethereum_tx.gas_price, gas_price ) return None assert multisig_tx.ethereum_tx.block_id is None, 'Block is present!' safe = Safe(multisig_tx.safe_id, self.ethereum_client) try: if safe.retrieve_nonce() > multisig_tx.nonce: multisig_tx.delete() # Transaction is not valid anymore return None except (ValueError, BadFunctionCallOutput): logger.error('Something is wrong with Safe %s, cannot retrieve nonce', multisig_tx.safe_id, exc_info=True) return None logger.info( '%s tx gas price was %d. Resending with new gas price %d', multisig_tx.ethereum_tx_id, multisig_tx.ethereum_tx.gas_price, gas_price ) safe_tx = multisig_tx.get_safe_tx(self.ethereum_client) tx_gas = safe_tx.recommended_gas() try: tx_hash, tx = safe_tx.execute(self.tx_sender_account.key, tx_gas=tx_gas, tx_gas_price=gas_price, tx_nonce=multisig_tx.ethereum_tx.nonce) except ValueError: # ValueError({'code': -32010, 'message': 'Transaction nonce is too low. Try incrementing the nonce.'}) try: # Check that transaction is still valid safe_tx.call(tx_sender_address=self.tx_sender_account.address, tx_gas=tx_gas) except InvalidMultisigTx: # Maybe there's a transaction with a lower nonce that must be mined before # It doesn't matter, as soon as a transaction with a newer nonce is added it will be deleted return None # Send transaction again with a new nonce with EthereumNonceLock(self.redis, self.ethereum_client, self.tx_sender_account.address, lock_timeout=60 * 2) as tx_nonce: tx_hash, tx = safe_tx.execute(self.tx_sender_account.key, tx_gas=tx_gas, tx_gas_price=gas_price, tx_nonce=tx_nonce) multisig_tx.ethereum_tx = EthereumTx.objects.create_from_tx_dict(tx, tx_hash) multisig_tx.full_clean(validate_unique=False) multisig_tx.save(update_fields=['ethereum_tx']) return multisig_tx.ethereum_tx
def get_last_used_nonce(self, safe_address: str) -> Optional[int]: safe = Safe(safe_address, self.ethereum_client) last_used_nonce = SafeMultisigTx.objects.get_last_nonce_for_safe(safe_address) last_used_nonce = last_used_nonce if last_used_nonce is not None else -1 try: blockchain_nonce = safe.retrieve_nonce() last_used_nonce = max(last_used_nonce, blockchain_nonce - 1) if last_used_nonce < 0: # There's no last_used_nonce last_used_nonce = None return last_used_nonce except BadFunctionCallOutput: # If Safe does not exist raise SafeDoesNotExist(f'Safe={safe_address} does not exist')
def setUpClass(cls) -> None: cls.ethereum_node_url = cls.ETHEREUM_NODE_URL cls.ethereum_client = EthereumClient(cls.ethereum_node_url) cls.w3 = cls.ethereum_client.w3 cls.ethereum_test_account = Account.from_key(cls.ETHEREUM_ACCOUNT_KEY) cls.safe_contract_address = Safe.deploy_master_contract(cls.ethereum_client, cls.ethereum_test_account).contract_address cls.safe_old_contract_address = Safe.deploy_master_contract_v1_0_0(cls.ethereum_client, cls.ethereum_test_account).contract_address cls.proxy_factory = ProxyFactory( ProxyFactory.deploy_proxy_factory_contract(cls.ethereum_client, cls.ethereum_test_account).contract_address, cls.ethereum_client)
def save(self, **kwargs): safe_address = self.context['safe_address'] ethereum_client = EthereumClientProvider() safe = Safe(safe_address, ethereum_client) try: safe_tx_gas = safe.estimate_tx_gas( self.validated_data['to'], self.validated_data['value'], self.validated_data['data'], self.validated_data['operation']) except IOError as exc: raise NodeConnectionError( f'Node connection error when estimating gas for safe {safe_address}' ) from exc return {'safe_tx_gas': safe_tx_gas}
def validate(self, data): super().validate(data) if not SafeContract.objects.filter(address=data['safe']).exists(): raise ValidationError( f"Safe={data['safe']} does not exist or it's still not indexed" ) ethereum_client = EthereumClientProvider() safe = Safe(data['safe'], ethereum_client) # Check owners and pending owners try: safe_owners = safe.retrieve_owners(block_identifier='pending') except BadFunctionCallOutput: # Error using pending block identifier safe_owners = safe.retrieve_owners(block_identifier='latest') signature = data['signature'] delegate = data['delegate'] operation_hash = DelegateSignatureHelper.calculate_hash(delegate) safe_signatures = SafeSignature.parse_signature( signature, operation_hash) if not safe_signatures: raise ValidationError('Cannot a valid signature') elif len(safe_signatures) > 1: raise ValidationError( 'More than one signatures detected, just one is expected') safe_signature = safe_signatures[0] delegator = safe_signature.owner if delegator not in safe_owners: if safe_signature.signature_type == SafeSignatureType.EOA: # Maybe it's an `eth_sign` signature without Gnosis Safe `v + 4`, let's try safe_signatures = SafeSignature.parse_signature( signature, DelegateSignatureHelper.calculate_hash(delegate, eth_sign=True)) safe_signature = safe_signatures[0] delegator = safe_signature.owner if delegator not in safe_owners: raise ValidationError( 'Signing owner is not an owner of the Safe') if not safe_signature.is_valid(): raise ValidationError( f'Signature of type={safe_signature.signature_type.name} for delegator={delegator} ' f'is not valid') data['delegator'] = delegator return data
def test_safe_creation(self): salt_nonce = generate_salt_nonce() owners = [Account.create().address for _ in range(2)] data = { 'saltNonce': salt_nonce, 'owners': owners, 'threshold': len(owners) } response = self.client.post(reverse('v3:safe-creation'), data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) response_json = response.json() safe_address = response_json['safe'] self.assertTrue(check_checksum(safe_address)) self.assertTrue(check_checksum(response_json['paymentReceiver'])) self.assertEqual(response_json['paymentToken'], NULL_ADDRESS) self.assertEqual(int(response_json['payment']), int(response_json['gasEstimated']) * int(response_json['gasPriceEstimated'])) self.assertGreater(int(response_json['gasEstimated']), 0) self.assertGreater(int(response_json['gasPriceEstimated']), 0) self.assertGreater(len(response_json['setupData']), 2) self.assertEqual(response_json['masterCopy'], settings.SAFE_CONTRACT_ADDRESS) self.assertTrue(SafeContract.objects.filter(address=safe_address)) self.assertTrue(SafeCreation2.objects.filter(owners__contains=[owners[0]])) safe_creation = SafeCreation2.objects.get(safe=safe_address) self.assertEqual(safe_creation.payment_token, None) # Payment includes deployment gas + gas to send eth to the deployer self.assertEqual(safe_creation.payment, safe_creation.wei_estimated_deploy_cost()) # Deploy the Safe to check it self.send_ether(safe_address, int(response_json['payment'])) safe_creation2 = SafeCreationServiceProvider().deploy_create2_safe_tx(safe_address) self.ethereum_client.get_transaction_receipt(safe_creation2.tx_hash, timeout=20) safe = Safe(safe_address, self.ethereum_client) self.assertEqual(safe.retrieve_master_copy_address(), response_json['masterCopy']) self.assertEqual(safe.retrieve_owners(), owners) # Test exception when same Safe is created response = self.client.post(reverse('v3:safe-creation'), data, format='json') self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) self.assertIn('SafeAlreadyExistsException', response.json()['exception']) data = { 'salt_nonce': -1, 'owners': owners, 'threshold': 2 } response = self.client.post(reverse('v3:safe-creation'), data, format='json') self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY)
def build_test_safe( self, number_owners: int = 3, threshold: Optional[int] = None, owners: Optional[List[str]] = None, fallback_handler: Optional[str] = None, ) -> SafeCreate2Tx: salt_nonce = generate_salt_nonce() owners = (owners if owners else [Account.create().address for _ in range(number_owners)]) threshold = threshold if threshold else len(owners) - 1 gas_price = self.ethereum_client.w3.eth.gas_price return Safe.build_safe_create2_tx( self.ethereum_client, self.safe_contract_address, self.proxy_factory_contract_address, salt_nonce, owners, threshold, fallback_handler=fallback_handler, gas_price=gas_price, payment_token=None, fixed_creation_cost=0, )
def estimate_safe_creation2( self, number_owners: int, payment_token: Optional[str] = None) -> SafeCreationEstimate: """ :param number_owners: :param payment_token: :return: :raises: InvalidPaymentToken """ payment_token = payment_token or NULL_ADDRESS payment_token_eth_value = self._get_token_eth_value_or_raise( payment_token) gas_price = self._get_configured_gas_price() fixed_creation_cost = self.safe_fixed_creation_cost return Safe.estimate_safe_creation_2( self.ethereum_client, self.safe_contract_address, self.proxy_factory.address, number_owners, gas_price, payment_token, payment_receiver=self.funder_account.address, fallback_handler=self.default_callback_handler, payment_token_eth_value=payment_token_eth_value, fixed_creation_cost=fixed_creation_cost)
def __init__(self, address: str, node_url: str): self.address = address self.node_url = node_url self.ethereum_client = EthereumClient(self.node_url) self.ens = ENS.fromWeb3(self.ethereum_client.w3) self.network: EthereumNetwork = self.ethereum_client.get_network() self.network_name: str = self.network.name self.network_number: int = self.network.value self.etherscan = Etherscan.from_network_number(self.network_number) self.safe_tx_service_url = TransactionService.from_network_number(self.network_number) self.safe_relay_service_url = RelayService.from_network_number(self.network_number) self.safe = Safe(address, self.ethereum_client) self.safe_contract = self.safe.get_contract() self.accounts: Set[Account] = set() self.default_sender: Optional[Account] = None self.executed_transactions: List[str] = [] self._safe_cli_info: Optional[SafeCliInfo] = None # Cache for SafeCliInfo
def test_change_master_copy(self): safe_operator = self.setup_operator() safe = Safe(safe_operator.address, self.ethereum_client) current_master_copy = safe.retrieve_master_copy_address() with self.assertRaises(SameMasterCopyException): safe_operator.change_master_copy(current_master_copy) random_address = Account.create().address with self.assertRaises(InvalidMasterCopyException): safe_operator.change_master_copy(random_address) self.assertTrue( safe_operator.change_master_copy(self.safe_old_contract_address)) self.assertEqual(safe_operator.safe_cli_info.master_copy, self.safe_old_contract_address) self.assertEqual(safe.retrieve_master_copy_address(), self.safe_old_contract_address)
def test_remove_owner(self): safe_address = self.deploy_test_safe( owners=[self.ethereum_test_account.address]).safe_address safe_operator = SafeOperator(safe_address, self.ethereum_node_url) random_address = Account.create().address with self.assertRaises(NonExistingOwnerException): safe_operator.remove_owner(random_address) safe_operator.load_cli_owners([self.ethereum_test_account.key.hex()]) new_owner = Account.create().address safe = Safe(safe_address, self.ethereum_client) self.assertTrue(safe_operator.add_owner(new_owner)) self.assertIn(new_owner, safe.retrieve_owners()) self.assertTrue(safe_operator.remove_owner(new_owner)) self.assertNotIn(new_owner, safe_operator.accounts) self.assertNotIn(new_owner, safe.retrieve_owners())
def test_change_fallback_handler(self): safe_operator = self.setup_operator() safe = Safe(safe_operator.address, self.ethereum_client) current_fallback_handler = safe.retrieve_fallback_handler() with self.assertRaises(SameFallbackHandlerException): safe_operator.change_fallback_handler(current_fallback_handler) new_fallback_handler = Account.create().address self.assertTrue( safe_operator.change_fallback_handler(new_fallback_handler)) self.assertEqual(safe_operator.safe_cli_info.fallback_handler, new_fallback_handler) self.assertEqual(safe.retrieve_fallback_handler(), new_fallback_handler) safe_operator.change_master_copy(self.safe_old_contract_address) with self.assertRaises(FallbackHandlerNotSupportedException): safe_operator.change_fallback_handler(Account.create().address)
def get_safe_info_from_blockchain( self, safe_address: ChecksumAddress) -> SafeInfo: """ :param safe_address: :return: SafeInfo from blockchain """ try: safe = Safe(safe_address, self.ethereum_client) safe_info = safe.retrieve_all_info() # Return same master copy information than the db method return replace( safe_info, version=SafeMasterCopy.objects.get_version_for_address( safe_info.master_copy), ) except IOError as exc: raise NodeConnectionException from exc except CannotRetrieveSafeInfoException as exc: raise CannotGetSafeInfoFromBlockchain(safe_address) from exc
def retrieve_safe_info(self, address: str) -> SafeInfo: safe = Safe(address, self.ethereum_client) if not self.ethereum_client.is_contract(address): raise SafeNotDeployed('Safe with address=%s not deployed' % address) nonce = safe.retrieve_nonce() threshold = safe.retrieve_threshold() owners = safe.retrieve_owners() master_copy = safe.retrieve_master_copy_address() version = safe.retrieve_version() fallback_handler = safe.retrieve_fallback_handler() return SafeInfo(address, nonce, threshold, owners, master_copy, version, fallback_handler)
def test_add_owner(self): safe_address = self.deploy_test_safe( owners=[self.ethereum_test_account.address]).safe_address safe_operator = SafeOperator(safe_address, self.ethereum_node_url) with self.assertRaises(ExistingOwnerException): safe_operator.add_owner(self.ethereum_test_account.address) new_owner = Account.create().address with self.assertRaises(SenderRequiredException): safe_operator.add_owner(new_owner) safe_operator.default_sender = self.ethereum_test_account with self.assertRaises(NotEnoughSignatures): safe_operator.add_owner(new_owner) safe_operator.accounts.add(self.ethereum_test_account) safe = Safe(safe_address, self.ethereum_client) self.assertTrue(safe_operator.add_owner(new_owner)) self.assertIn(self.ethereum_test_account, safe_operator.accounts) self.assertIn(new_owner, safe.retrieve_owners())
def save(self, **kwargs): safe_address = self.context["safe_address"] ethereum_client = EthereumClientProvider() safe = Safe(safe_address, ethereum_client) exc = None # Retry thrice to get an estimation for _ in range(3): try: safe_tx_gas = safe.estimate_tx_gas( self.validated_data["to"], self.validated_data["value"], self.validated_data["data"], self.validated_data["operation"], ) return {"safe_tx_gas": safe_tx_gas} except (IOError, ValueError) as _exc: exc = _exc raise NodeConnectionException( f"Node connection error when estimating gas for Safe {safe_address}" ) from exc
def test_change_guard(self): safe_operator = self.setup_operator(version="1.1.1") with self.assertRaises(GuardNotSupportedException): safe_operator.change_guard(Account.create().address) safe_operator = self.setup_operator(version="1.3.0") safe = Safe(safe_operator.address, self.ethereum_client) current_guard = safe.retrieve_guard() with self.assertRaises(SameGuardException): safe_operator.change_guard(current_guard) new_guard = Account.create().address with self.assertRaises(InvalidGuardException): # Contract does not exist self.assertTrue(safe_operator.change_guard(new_guard)) with mock.patch.object( EthereumClient, "is_contract", autospec=True, return_value=True ): self.assertTrue(safe_operator.change_guard(new_guard)) self.assertEqual(safe_operator.safe_cli_info.guard, new_guard) self.assertEqual(safe.retrieve_guard(), new_guard)
def estimate_tx_for_all_tokens(self, safe_address: str, to: str, value: int, data: bytes, operation: int) -> TransactionEstimationWithNonceAndGasTokens: """ :return: TransactionEstimation with costs using ether and every gas token supported by the service, with the last used nonce of the Safe :raises: InvalidGasToken: If Gas Token is not valid """ safe = Safe(safe_address, self.ethereum_client) last_used_nonce = self.get_last_used_nonce(safe_address) safe_tx_gas = safe.estimate_tx_gas(to, value, data, operation) safe_version = safe.retrieve_version() if Version(safe_version) >= Version('1.0.0'): safe_tx_operational_gas = 0 else: safe_tx_operational_gas = safe.estimate_tx_operational_gas(len(data) if data else 0) # Calculate `base_gas` for ether and calculate for tokens using the ether token price ether_safe_tx_base_gas = safe.estimate_tx_base_gas(to, value, data, operation, NULL_ADDRESS, safe_tx_gas) base_gas_price = self._get_configured_gas_price() gas_price = self._estimate_tx_gas_price(base_gas_price, NULL_ADDRESS) gas_token_estimations = [TransactionGasTokenEstimation(ether_safe_tx_base_gas, gas_price, NULL_ADDRESS)] token_gas_difference = 50000 # 50K gas more expensive than ether for token in Token.objects.gas_tokens(): try: gas_price = self._estimate_tx_gas_price(base_gas_price, token.address) gas_token_estimations.append( TransactionGasTokenEstimation(ether_safe_tx_base_gas + token_gas_difference, gas_price, token.address) ) except CannotGetTokenPriceFromApi: logger.error('Cannot get price for token=%s', token.address) return TransactionEstimationWithNonceAndGasTokens(last_used_nonce, safe_tx_gas, safe_tx_operational_gas, gas_token_estimations)