class SafeMultisigTransactionResponseSerializer(SafeMultisigTxSerializerV1): execution_date = serializers.DateTimeField() submission_date = serializers.DateTimeField(source='created') # First seen by this service modified = serializers.DateTimeField() block_number = serializers.SerializerMethodField() transaction_hash = Sha3HashField(source='ethereum_tx_id') safe_tx_hash = Sha3HashField() executor = serializers.SerializerMethodField() value = serializers.CharField() is_executed = serializers.BooleanField(source='executed') is_successful = serializers.SerializerMethodField() gas_price = serializers.CharField() eth_gas_price = serializers.SerializerMethodField() gas_used = serializers.SerializerMethodField() fee = serializers.SerializerMethodField() origin = serializers.CharField() data_decoded = serializers.SerializerMethodField() confirmations_required = serializers.IntegerField() confirmations = serializers.SerializerMethodField() signatures = HexadecimalField() def get_block_number(self, obj: MultisigTransaction) -> Optional[int]: if obj.ethereum_tx_id: return obj.ethereum_tx.block_id def get_confirmations(self, obj: MultisigTransaction) -> Dict[str, Any]: """ Filters confirmations queryset :param obj: MultisigConfirmation instance :return: Serialized queryset """ return SafeMultisigConfirmationResponseSerializer(obj.confirmations, many=True).data def get_executor(self, obj: MultisigTransaction) -> Optional[str]: if obj.ethereum_tx_id: return obj.ethereum_tx._from def get_fee(self, obj: MultisigTransaction) -> Optional[int]: if obj.ethereum_tx: if obj.ethereum_tx.gas_used and obj.ethereum_tx.gas_price: return str(obj.ethereum_tx.gas_used * obj.ethereum_tx.gas_price) def get_eth_gas_price(self, obj: MultisigTransaction) -> Optional[str]: if obj.ethereum_tx and obj.ethereum_tx.gas_price: return str(obj.ethereum_tx.gas_price) def get_gas_used(self, obj: MultisigTransaction) -> Optional[int]: if obj.ethereum_tx and obj.ethereum_tx.gas_used: return obj.ethereum_tx.gas_used def get_is_successful(self, obj: MultisigTransaction) -> Optional[bool]: if obj.failed is None: return None else: return not obj.failed def get_data_decoded(self, obj: MultisigTransaction) -> Dict[str, Any]: return get_data_decoded_from_data(obj.data.tobytes() if obj.data else b'')
class TransferResponseSerializer(serializers.Serializer): type = serializers.SerializerMethodField() execution_date = serializers.DateTimeField() block_number = serializers.IntegerField() transaction_hash = Sha3HashField() to = EthereumAddressField() from_ = EthereumAddressField(source='_from') value = serializers.CharField() token_id = serializers.CharField() token_address = EthereumAddressField(allow_null=True, default=None) def get_fields(self): result = super().get_fields() # Rename `from_` to `from` from_ = result.pop('from_') result['from'] = from_ return result def get_type(self, obj: Dict[str, Any]) -> str: if not obj.get('token_address'): return TransferType.ETHER_TRANSFER.name else: if obj.get('value') is not None: return TransferType.ERC20_TRANSFER.name elif obj.get('token_id') is not None: return TransferType.ERC721_TRANSFER.name return TransferType.UNKNOWN
class SafeCreationInfoResponseSerializer(serializers.Serializer): created = serializers.DateTimeField() creator = EthereumAddressField() factory_address = EthereumAddressField() master_copy = EthereumAddressField(allow_null=True) setup_data = HexadecimalField(allow_null=True) transaction_hash = Sha3HashField()
class TransferResponseSerializer(serializers.Serializer): type = serializers.SerializerMethodField() execution_date = serializers.DateTimeField() block_number = serializers.IntegerField(source="block") transaction_hash = Sha3HashField() to = EthereumAddressField() from_ = EthereumAddressField(source="_from", allow_zero_address=True) value = serializers.CharField(allow_null=True, source="_value") token_id = serializers.CharField(allow_null=True, source="_token_id") token_address = EthereumAddressField(allow_null=True, default=None) def get_fields(self): result = super().get_fields() # Rename `from_` to `from` from_ = result.pop("from_") result["from"] = from_ return result def get_type(self, obj: TransferDict) -> str: if obj["token_address"] is None: return TransferType.ETHER_TRANSFER.name else: if obj["_value"] is not None: return TransferType.ERC20_TRANSFER.name elif obj["_token_id"] is not None: return TransferType.ERC721_TRANSFER.name else: return TransferType.UNKNOWN.name def validate(self, data): super().validate(data) if data["value"] is None and data["token_id"] is None: raise ValidationError("Both value and token_id cannot be null") return data
class SafeMultisigTxResponseSerializer(serializers.Serializer): to = EthereumAddressField(allow_null=True, allow_zero_address=True) ethereum_tx = EthereumTxSerializer() value = serializers.IntegerField(min_value=0) data = HexadecimalField() timestamp = serializers.DateTimeField(source='created') operation = serializers.SerializerMethodField() safe_tx_gas = serializers.IntegerField(min_value=0) data_gas = serializers.IntegerField(min_value=0) gas_price = serializers.IntegerField(min_value=0) gas_token = EthereumAddressField(allow_null=True, allow_zero_address=True) refund_receiver = EthereumAddressField(allow_null=True, allow_zero_address=True) nonce = serializers.IntegerField(min_value=0) safe_tx_hash = Sha3HashField() tx_hash = serializers.SerializerMethodField() transaction_hash = serializers.SerializerMethodField( method_name='get_tx_hash') # Retro compatibility def get_operation(self, obj): """ Filters confirmations queryset :param obj: MultisigConfirmation instance :return: serialized queryset """ return SafeOperation(obj.operation).name def get_tx_hash(self, obj): tx_hash = obj.ethereum_tx.tx_hash if tx_hash and isinstance(tx_hash, bytes): return tx_hash.hex() return tx_hash
class SafeCreationResponseSerializer(serializers.Serializer): signature = SignatureResponseSerializer() tx = TransactionResponseSerializer() tx_hash = Sha3HashField() payment = serializers.CharField() payment_token = EthereumAddressField(allow_null=True, allow_zero_address=True) safe = EthereumAddressField() deployer = EthereumAddressField() funder = EthereumAddressField()
class SafeCreationInfoResponseSerializer(serializers.Serializer): created = serializers.DateTimeField() creator = EthereumAddressField() transaction_hash = Sha3HashField() factory_address = EthereumAddressField() master_copy = EthereumAddressField(allow_null=True) setup_data = HexadecimalField(allow_null=True) data_decoded = serializers.SerializerMethodField() def get_data_decoded(self, obj: SafeCreationInfo) -> Dict[str, Any]: return get_data_decoded_from_data(obj.setup_data or b'')
class IncomingTransactionResponseSerializer(serializers.Serializer): execution_date = serializers.DateTimeField() block_number = serializers.IntegerField() transaction_hash = Sha3HashField() to = EthereumAddressField() from_ = EthereumAddressField(source='_from') value = serializers.CharField() token_address = EthereumAddressField(allow_null=True, default=None) def get_fields(self): result = super().get_fields() # Rename `from_` to `from` from_ = result.pop('from_') result['from'] = from_ return result
class SafeMultisigTransactionSerializer(SafeMultisigTxSerializerV1): contract_transaction_hash = Sha3HashField() sender = EthereumAddressField() # TODO Make signature mandatory signature = HexadecimalField( required=False, min_length=130) # Signatures must be at least 65 bytes origin = serializers.CharField(max_length=100, allow_null=True, default=None) def validate(self, data): super().validate(data) ethereum_client = EthereumClientProvider() safe = Safe(data['safe'], ethereum_client) safe_tx = safe.build_multisig_tx(data['to'], data['value'], data['data'], data['operation'], data['safe_tx_gas'], data['base_gas'], data['gas_price'], data['gas_token'], data['refund_receiver'], safe_nonce=data['nonce']) contract_transaction_hash = safe_tx.safe_tx_hash # Check safe tx hash matches if contract_transaction_hash != data['contract_transaction_hash']: raise ValidationError( f'Contract-transaction-hash={contract_transaction_hash.hex()} ' f'does not match provided contract-tx-hash={data["contract_transaction_hash"].hex()}' ) # Check there's not duplicated tx with same `nonce` or same `safeTxHash` for the same Safe. # We allow duplicated if existing tx is not executed multisig_transactions = MultisigTransaction.objects.filter( safe=safe.address, nonce=data['nonce']).executed() if multisig_transactions: for multisig_transaction in multisig_transactions: if multisig_transaction.safe_tx_hash == contract_transaction_hash.hex( ): raise ValidationError( f'Tx with safe-tx-hash={contract_transaction_hash.hex()} ' f'for safe={safe.address} was already executed in ' f'tx-hash={multisig_transaction.ethereum_tx_id}') raise ValidationError( f'Tx with nonce={safe_tx.safe_nonce} for safe={safe.address} ' f'already executed in tx-hash={multisig_transactions[0].ethereum_tx_id}' ) # 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') data['safe_owners'] = safe_owners delegates = SafeContractDelegate.objects.get_delegates_for_safe( safe.address) allowed_senders = safe_owners + delegates if not data['sender'] in allowed_senders: raise ValidationError( f'Sender={data["sender"]} is not an owner or delegate. ' f'Current owners={safe_owners}. Delegates={delegates}') signature_owners = [] # TODO Make signature mandatory signature = data.get('signature', b'') parsed_signatures = SafeSignature.parse_signature( signature, contract_transaction_hash) data['parsed_signatures'] = parsed_signatures for safe_signature in parsed_signatures: owner = safe_signature.owner 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 delegates and len(parsed_signatures) > 1: raise ValidationError( f'Just one signature is expected if using delegates') if owner not in allowed_senders: raise ValidationError( f'Signer={owner} is not an owner or delegate. ' f'Current owners={safe_owners}. Delegates={delegates}') if owner in signature_owners: raise ValidationError( f'Signature for owner={owner} is duplicated') signature_owners.append(owner) # TODO Make signature mandatory. len(signature_owners) must be >= 1 if signature_owners and data['sender'] not in signature_owners: raise ValidationError( f'Signature does not match sender={data["sender"]}. ' f'Calculated owners={signature_owners}') return data def save(self, **kwargs): safe_tx_hash = self.validated_data['contract_transaction_hash'] multisig_transaction, _ = MultisigTransaction.objects.get_or_create( safe_tx_hash=safe_tx_hash, defaults={ 'safe': self.validated_data['safe'], 'to': self.validated_data['to'], 'value': self.validated_data['value'], 'data': self.validated_data['data'] if self.validated_data['data'] else None, 'operation': self.validated_data['operation'], 'safe_tx_gas': self.validated_data['safe_tx_gas'], 'base_gas': self.validated_data['base_gas'], 'gas_price': self.validated_data['gas_price'], 'gas_token': self.validated_data['gas_token'], 'refund_receiver': self.validated_data['refund_receiver'], 'nonce': self.validated_data['nonce'], 'origin': self.validated_data['origin'], }) for safe_signature in self.validated_data.get('parsed_signatures'): owner = safe_signature.owner if safe_signature.owner in self.validated_data['safe_owners']: multisig_confirmation, _ = MultisigConfirmation.objects.get_or_create( multisig_transaction_hash=safe_tx_hash, owner=owner, defaults={ 'multisig_transaction': multisig_transaction, 'signature': safe_signature.export_signature(), 'signature_type': safe_signature.signature_type.value, }) return multisig_transaction
class SafeFunding2ResponseSerializer(serializers.Serializer): block_number = serializers.CharField() tx_hash = Sha3HashField()
class SafeCreationInfoResponseSerializer(serializers.Serializer): created = serializers.DateTimeField() transaction_hash = Sha3HashField() creator = EthereumAddressField()
class SafeMultisigTransactionSerializer(SafeMultisigTxSerializerV1): contract_transaction_hash = Sha3HashField() sender = EthereumAddressField() signature = HexadecimalField(required=False) origin = serializers.CharField(max_length=100, allow_null=True, default=None) def validate(self, data): super().validate(data) ethereum_client = EthereumClientProvider() safe = Safe(data['safe'], ethereum_client) safe_tx = safe.build_multisig_tx(data['to'], data['value'], data['data'], data['operation'], data['safe_tx_gas'], data['base_gas'], data['gas_price'], data['gas_token'], data['refund_receiver'], safe_nonce=data['nonce']) contract_transaction_hash = safe_tx.safe_tx_hash # Check safe tx hash matches if contract_transaction_hash != data['contract_transaction_hash']: raise ValidationError( f'Contract-transaction-hash={contract_transaction_hash.hex()} ' f'does not match provided contract-tx-hash={data["contract_transaction_hash"].hex()}' ) # Check there's not duplicated tx with same `nonce` for the same Safe. # We allow duplicated if existing tx is not executed try: multisig_transaction: MultisigTransaction = MultisigTransaction.objects.exclude( ethereum_tx=None).exclude( safe_tx_hash=contract_transaction_hash).get( safe=safe.address, nonce=data['nonce']) if multisig_transaction.safe_tx_hash != contract_transaction_hash: raise ValidationError( f'Tx with nonce={safe_tx.safe_nonce} for safe={safe.address} already executed in ' f'tx-hash={multisig_transaction.ethereum_tx_id}') except MultisigTransaction.DoesNotExist: pass # Check owners and old owners, owner might be removed but that tx can still be signed by that owner if not safe.retrieve_is_owner(data['sender']): try: # TODO Fix this, we can use SafeStatus now if not safe.retrieve_is_owner( data['sender'], block_identifier=max( 0, ethereum_client.current_block_number - 20)): raise ValidationError('User is not an owner') except BadFunctionCallOutput: # If it didn't exist 20 blocks ago raise ValidationError('User is not an owner') signature = data.get('signature') if signature is not None: # TODO Support signatures with multiple owners if len(signature) != 65: raise ValidationError( 'Signatures with more than one owner still not supported') safe_signature = SafeSignature(signature, contract_transaction_hash) # TODO Support contract signatures and approved hashes if safe_signature.signature_type == SafeSignatureType.CONTRACT_SIGNATURE: raise ValidationError('Contract signatures not supported') elif safe_signature.signature_type == SafeSignatureType.APPROVED_HASH: # Index it automatically later del data['signature'] address = safe_signature.owner if address != data['sender']: raise ValidationError( f'Signature does not match sender={data["sender"]}. Calculated owner={address}' ) return data def save(self, **kwargs): multisig_transaction, _ = MultisigTransaction.objects.get_or_create( safe_tx_hash=self.validated_data['contract_transaction_hash'], defaults={ 'safe': self.validated_data['safe'], 'to': self.validated_data['to'], 'value': self.validated_data['value'], 'data': self.validated_data['data'] if self.validated_data['data'] else None, 'operation': self.validated_data['operation'], 'safe_tx_gas': self.validated_data['safe_tx_gas'], 'base_gas': self.validated_data['base_gas'], 'gas_price': self.validated_data['gas_price'], 'gas_token': self.validated_data['gas_token'], 'refund_receiver': self.validated_data['refund_receiver'], 'nonce': self.validated_data['nonce'], 'origin': self.validated_data['origin'], }) if self.validated_data.get('signature'): MultisigConfirmation.objects.get_or_create( multisig_transaction_hash=multisig_transaction.safe_tx_hash, owner=self.validated_data['sender'], defaults={ 'multisig_transaction': multisig_transaction, 'signature': self.validated_data.get('signature'), }) return multisig_transaction
class SafeMultisigTransactionResponseSerializer(SafeMultisigTxSerializerV1): execution_date = serializers.DateTimeField() submission_date = serializers.DateTimeField( source="created") # First seen by this service modified = serializers.DateTimeField() block_number = serializers.SerializerMethodField() transaction_hash = Sha3HashField(source="ethereum_tx_id") safe_tx_hash = Sha3HashField() executor = serializers.SerializerMethodField() value = serializers.CharField() is_executed = serializers.BooleanField(source="executed") is_successful = serializers.SerializerMethodField() gas_price = serializers.CharField() eth_gas_price = serializers.SerializerMethodField() max_fee_per_gas = serializers.SerializerMethodField() max_priority_fee_per_gas = serializers.SerializerMethodField() gas_used = serializers.SerializerMethodField() fee = serializers.SerializerMethodField() origin = serializers.CharField() data_decoded = serializers.SerializerMethodField() confirmations_required = serializers.IntegerField() confirmations = serializers.SerializerMethodField() trusted = serializers.BooleanField() signatures = HexadecimalField(allow_null=True, required=False) def get_block_number(self, obj: MultisigTransaction) -> Optional[int]: if obj.ethereum_tx_id: return obj.ethereum_tx.block_id @swagger_serializer_method( serializer_or_field=SafeMultisigConfirmationResponseSerializer) def get_confirmations(self, obj: MultisigTransaction) -> Dict[str, Any]: """ Filters confirmations queryset :param obj: MultisigConfirmation instance :return: Serialized queryset """ return SafeMultisigConfirmationResponseSerializer(obj.confirmations, many=True).data def get_executor(self, obj: MultisigTransaction) -> Optional[str]: if obj.ethereum_tx_id: return obj.ethereum_tx._from def get_fee(self, obj: MultisigTransaction) -> Optional[int]: if obj.ethereum_tx: if obj.ethereum_tx.gas_used and obj.ethereum_tx.gas_price: return str(obj.ethereum_tx.gas_used * obj.ethereum_tx.gas_price) def get_eth_gas_price(self, obj: MultisigTransaction) -> Optional[str]: if obj.ethereum_tx and obj.ethereum_tx.gas_price: return str(obj.ethereum_tx.gas_price) def get_max_fee_per_gas(self, obj: MultisigTransaction) -> Optional[str]: if obj.ethereum_tx and obj.ethereum_tx.max_fee_per_gas: return str(obj.ethereum_tx.max_fee_per_gas) def get_max_priority_fee_per_gas( self, obj: MultisigTransaction) -> Optional[str]: if obj.ethereum_tx and obj.ethereum_tx.max_priority_fee_per_gas: return str(obj.ethereum_tx.max_priority_fee_per_gas) def get_gas_used(self, obj: MultisigTransaction) -> Optional[int]: if obj.ethereum_tx and obj.ethereum_tx.gas_used: return obj.ethereum_tx.gas_used def get_is_successful(self, obj: MultisigTransaction) -> Optional[bool]: if obj.failed is None: return None else: return not obj.failed def get_data_decoded(self, obj: MultisigTransaction) -> Dict[str, Any]: # If delegate call contract must be whitelisted (security) if obj.data_should_be_decoded(): return get_data_decoded_from_data( obj.data.tobytes() if obj.data else b"", address=obj.to)
class SafeMultisigTransactionSerializer(SafeMultisigTxSerializerV1): contract_transaction_hash = Sha3HashField() sender = EthereumAddressField() # TODO Make signature mandatory signature = HexadecimalField( allow_null=True, required=False, min_length=65) # Signatures must be at least 65 bytes origin = serializers.CharField(max_length=200, allow_null=True, default=None) def validate(self, data): super().validate(data) ethereum_client = EthereumClientProvider() safe = Safe(data["safe"], ethereum_client) try: safe_version = safe.retrieve_version() except BadFunctionCallOutput as e: raise ValidationError( f"Could not get Safe version from blockchain, check contract exists on network " f"{ethereum_client.get_network().name}") from e except IOError: raise ValidationError( "Problem connecting to the ethereum node, please try again later" ) safe_tx = safe.build_multisig_tx( data["to"], data["value"], data["data"], data["operation"], data["safe_tx_gas"], data["base_gas"], data["gas_price"], data["gas_token"], data["refund_receiver"], safe_nonce=data["nonce"], safe_version=safe_version, ) contract_transaction_hash = safe_tx.safe_tx_hash # Check safe tx hash matches if contract_transaction_hash != data["contract_transaction_hash"]: raise ValidationError( f"Contract-transaction-hash={contract_transaction_hash.hex()} " f'does not match provided contract-tx-hash={data["contract_transaction_hash"].hex()}' ) # Check there's not duplicated tx with same `nonce` or same `safeTxHash` for the same Safe. # We allow duplicated if existing tx is not executed multisig_transactions = MultisigTransaction.objects.filter( safe=safe.address, nonce=data["nonce"]).executed() if multisig_transactions: for multisig_transaction in multisig_transactions: if multisig_transaction.safe_tx_hash == contract_transaction_hash.hex( ): raise ValidationError( f"Tx with safe-tx-hash={contract_transaction_hash.hex()} " f"for safe={safe.address} was already executed in " f"tx-hash={multisig_transaction.ethereum_tx_id}") raise ValidationError( f"Tx with nonce={safe_tx.safe_nonce} for safe={safe.address} " f"already executed in tx-hash={multisig_transactions[0].ethereum_tx_id}" ) # 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") except IOError: raise ValidationError( "Problem connecting to the ethereum node, please try again later" ) data["safe_owners"] = safe_owners delegates = SafeContractDelegate.objects.get_delegates_for_safe_and_owners( safe.address, safe_owners) allowed_senders = set(safe_owners) | delegates if not data["sender"] in allowed_senders: raise ValidationError( f'Sender={data["sender"]} is not an owner or delegate. ' f"Current owners={safe_owners}. Delegates={delegates}") signature_owners = [] # TODO Make signature mandatory signature = data.get("signature", b"") parsed_signatures = SafeSignature.parse_signature( signature, contract_transaction_hash) data["parsed_signatures"] = parsed_signatures # If there's at least one signature, transaction is trusted (until signatures are mandatory) data["trusted"] = bool(parsed_signatures) for safe_signature in parsed_signatures: owner = safe_signature.owner 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 delegates and len(parsed_signatures) > 1: raise ValidationError( "Just one signature is expected if using delegates") if owner not in allowed_senders: raise ValidationError( f"Signer={owner} is not an owner or delegate. " f"Current owners={safe_owners}. Delegates={delegates}") if owner in signature_owners: raise ValidationError( f"Signature for owner={owner} is duplicated") signature_owners.append(owner) # TODO Make signature mandatory. len(signature_owners) must be >= 1 if signature_owners and data["sender"] not in signature_owners: raise ValidationError( f'Signature does not match sender={data["sender"]}. ' f"Calculated owners={signature_owners}") return data def save(self, **kwargs): safe_tx_hash = self.validated_data["contract_transaction_hash"] origin = self.validated_data["origin"] trusted = self.validated_data["trusted"] if not trusted: # Check user permission if (self.context and (request := self.context.get("request")) and (user := request.user)): trusted = user.has_perm("history.create_trusted")