def create_multisig_tx(self, safe_address: str, to: str, value: int, data: bytes, operation: int, safe_tx_gas: int, base_gas: int, gas_price: int, gas_token: str, refund_receiver: str, nonce: int, signatures: List[Dict[str, int]]) -> SafeMultisigTx: """ :return: Database model of SafeMultisigTx :raises: SafeMultisigTxExists: If Safe Multisig Tx with nonce already exists :raises: InvalidGasToken: If Gas Token is not valid :raises: TransactionServiceException: If Safe Tx is not valid (not sorted owners, bad signature, bad nonce...) """ safe_contract, _ = SafeContract.objects.get_or_create( address=safe_address, defaults={'master_copy': NULL_ADDRESS}) created = timezone.now() if SafeMultisigTx.objects.filter(safe=safe_contract, nonce=nonce).exists(): raise SafeMultisigTxExists( f'Tx with nonce={nonce} for safe={safe_address} already exists in DB' ) signature_pairs = [(s['v'], s['r'], s['s']) for s in signatures] signatures_packed = signatures_to_bytes(signature_pairs) try: tx_hash, safe_tx_hash, tx = self._send_multisig_tx( safe_address, to, value, data, operation, safe_tx_gas, base_gas, gas_price, gas_token, refund_receiver, nonce, signatures_packed) except SafeServiceException as exc: raise TransactionServiceException(str(exc)) from exc ethereum_tx = EthereumTx.objects.create_from_tx(tx, tx_hash) # Fix race conditions for tx being created at the same time try: return SafeMultisigTx.objects.create( created=created, safe=safe_contract, ethereum_tx=ethereum_tx, to=to, value=value, data=data, operation=operation, safe_tx_gas=safe_tx_gas, data_gas=base_gas, gas_price=gas_price, gas_token=None if gas_token == NULL_ADDRESS else gas_token, refund_receiver=refund_receiver, nonce=nonce, signatures=signatures_packed, safe_tx_hash=safe_tx_hash, ) except IntegrityError as exc: raise SafeMultisigTxExists( f'Tx with nonce={nonce} for safe={safe_address} already exists in DB' ) from exc
def test_safe_multisig_tx_post(self): # Create Safe ------------------------------------------------ w3 = self.ethereum_client.w3 safe_balance = w3.toWei(0.01, 'ether') accounts = [self.create_account(), self.create_account()] # Signatures must be sorted! accounts.sort(key=lambda account: account.address.lower()) owners = [x.address for x in accounts] threshold = len(accounts) safe_creation = self.deploy_test_safe(owners=owners, threshold=threshold, initial_funding_wei=safe_balance) my_safe_address = safe_creation.safe_address SafeContractFactory(address=my_safe_address) self.assertEqual(self.ethereum_client.get_balance(my_safe_address), safe_balance) # Safe prepared -------------------------------------------- to = Account.create().address value = safe_balance // 2 tx_data = None operation = SafeOperation.CALL.value data = { "to": to, "value": value, "data": tx_data, "operation": operation, } # Get estimation for gas response = self.client.post(reverse('v1:safe-multisig-tx-estimate', args=(my_safe_address, )), data=data, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) estimation_json = response.json() safe_tx_gas = estimation_json['safeTxGas'] + estimation_json[ 'operationalGas'] data_gas = estimation_json['dataGas'] gas_price = estimation_json['gasPrice'] gas_token = estimation_json['gasToken'] refund_receiver = None nonce = 0 multisig_tx_hash = SafeTx(None, my_safe_address, to, value, tx_data, operation, safe_tx_gas, data_gas, gas_price, gas_token, refund_receiver, safe_nonce=nonce).safe_tx_hash signatures = [ account.signHash(multisig_tx_hash) for account in accounts ] signatures_json = [{ 'v': s['v'], 'r': s['r'], 's': s['s'] } for s in signatures] data = { "to": to, "value": value, "data": tx_data, "operation": operation, "safe_tx_gas": safe_tx_gas, "data_gas": data_gas, "gas_price": gas_price, "gas_token": gas_token, "nonce": nonce, "signatures": signatures_json } response = self.client.post(reverse('v1:safe-multisig-txs', args=(my_safe_address, )), data=data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) tx_hash = response.json()['transactionHash'][2:] # Remove leading 0x safe_multisig_tx = SafeMultisigTx.objects.get( ethereum_tx__tx_hash=tx_hash) self.assertEqual(safe_multisig_tx.to, to) self.assertEqual(safe_multisig_tx.value, value) self.assertEqual(safe_multisig_tx.data, tx_data) self.assertEqual(safe_multisig_tx.operation, operation) self.assertEqual(safe_multisig_tx.safe_tx_gas, safe_tx_gas) self.assertEqual(safe_multisig_tx.data_gas, data_gas) self.assertEqual(safe_multisig_tx.gas_price, gas_price) self.assertEqual(safe_multisig_tx.gas_token, None) self.assertEqual(safe_multisig_tx.nonce, nonce) signature_pairs = [(s['v'], s['r'], s['s']) for s in signatures] signatures_packed = signatures_to_bytes(signature_pairs) self.assertEqual(bytes(safe_multisig_tx.signatures), signatures_packed) # Send the same tx again response = self.client.post(reverse('v1:safe-multisig-txs', args=(my_safe_address, )), data=data, format='json') self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) self.assertTrue('exists' in response.data['exception']) # Send with a Safe not created via the service safe_creation = self.deploy_test_safe(owners=owners, threshold=threshold, initial_funding_wei=safe_balance) my_safe_address = safe_creation.safe_address multisig_tx_hash = SafeTx(None, my_safe_address, to, value, tx_data, operation, safe_tx_gas, data_gas, gas_price, gas_token, refund_receiver, safe_nonce=nonce).safe_tx_hash signatures = [ account.signHash(multisig_tx_hash) for account in accounts ] signatures_json = [{ 'v': s['v'], 'r': s['r'], 's': s['s'] } for s in signatures] data['signatures'] = signatures_json with self.assertRaises(SafeContract.DoesNotExist): SafeContract.objects.get(address=my_safe_address) response = self.client.post(reverse('v1:safe-multisig-txs', args=(my_safe_address, )), data=data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertTrue( SafeContract.objects.filter(address=my_safe_address).exists()) self.assertEqual( SafeMultisigTx.objects.filter(safe_id=my_safe_address).count(), 1) # Send the same tx again response = self.client.post(reverse('v1:safe-multisig-txs', args=(my_safe_address, )), data=data, format='json') self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) self.assertTrue('exists' in response.data['exception']) self.assertEqual( SafeMultisigTx.objects.filter(safe_id=my_safe_address).count(), 1) # Send tx with not existing Safe my_safe_address = Account.create().address response = self.client.post(reverse('v1:safe-multisig-txs', args=(my_safe_address, )), data=data, format='json') self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) self.assertTrue('InvalidProxyContract' in response.data['exception'])