Beispiel #1
0
class Payment(models.Model):
    channel = models.ForeignKey(Channel,
                                on_delete=models.CASCADE,
                                related_name="payments")
    amount = EthereumTokenAmountField()
    timestamp = models.DateTimeField()
    identifier = models.BigIntegerField()
    sender_address = EthereumAddressField()
    receiver_address = EthereumAddressField()

    @property
    def url(self):
        return f"{self.raiden.api_root_url}/{self.token.address}/{self.partner_address}"

    @property
    def token(self):
        return self.channel.token

    @property
    def as_token_amount(self) -> EthereumTokenAmount:
        return EthereumTokenAmount(amount=self.amount, currency=self.token)

    @classmethod
    def make(cls, channel: Channel, **payment_data):
        payment, _ = channel.payments.get_or_create(channel=channel,
                                                    **payment_data)
        return payment

    class Meta:
        unique_together = ("channel", "timestamp", "sender_address",
                           "receiver_address")
Beispiel #2
0
class ExternalTransfer(Transfer):
    recipient_address = EthereumAddressField(db_index=True)

    @property
    def target(self):
        return self.recipient_address

    @transaction.atomic()
    def _execute(self):
        transfer_amount = self.as_token_amount
        channel = Channel.select_for_transfer(self.recipient_address, transfer_amount)
        if channel:
            payment_id = channel.send(
                self.recipient_address, transfer_amount, payment_identifier=self.identifier
            )
            RaidenTransaction.objects.create(
                transfer=self, channel=channel, payment_identifier=payment_id
            )
        else:
            account = EthereumAccount.select_for_transfer(transfer_amount)
            if account is None:
                raise TransferError("No channel nor account with funds to make transfer found")
            tx_hash = account.send(self.recipient_address, transfer_amount)
            BlockchainTransaction.objects.create(transfer=self, transaction_hash=tx_hash)
        transfer_executed.send_robust(sender=ExternalTransfer, transfer=self)

    @transaction.atomic()
    def _make_reserve(self):
        self.sender.balance_entries.create(amount=-self.amount, currency=self.currency)
        self.sender.reserves.update_or_create(
            usertransferreserve__transfer=self,
            defaults={"amount": self.amount, "currency": self.currency},
        )
Beispiel #3
0
class TokenNetwork(models.Model):
    address = EthereumAddressField()
    token = models.OneToOneField(EthereumToken, on_delete=models.CASCADE)

    @property
    def url(self):
        return f"{self.raiden.api_root_url}/tokens/{self.address}"

    def can_reach(self, address):
        # This is a very naive assumption. One should not assume that we can
        # reach an address just because the address has an open channel.

        # However, our main purpose is only to find out if a given address is
        # being used by raiden and that we can _try_ to use for a transfer.
        open_channels = self.channels.filter(status__status=CHANNEL_STATUSES.open)
        return open_channels.filter(participant_addresses__contains=[address]).exists()

    @property
    def events(self):
        return TokenNetworkChannelEvent.objects.filter(channel__token_network=self)

    def get_contract(self, w3: Web3):
        manager = ContractManager(contracts_precompiled_path())
        abi = manager.get_contract_abi(CONTRACT_TOKEN_NETWORK)
        return w3.eth.contract(abi=abi, address=self.address)

    @classmethod
    def make(cls, token: EthereumToken, token_network_contract: Contract):
        address = token_network_contract.functions.token_to_token_networks(token.address).call()
        token_network, _ = cls.objects.get_or_create(token=token, defaults={"address": address})
        return token_network

    def __str__(self):
        return f"{self.address} - ({self.token.code} @ {self.token.chain_id})"
Beispiel #4
0
class AbstractEthereumAccount(models.Model):
    address = EthereumAddressField(unique=True, db_index=True)

    def send(self, recipient_address, transfer_amount: EthereumTokenAmount) -> str:
        raise NotImplementedError()

    def sign_transaction(self, transaction_data, **kw):
        raise NotImplementedError()

    class Meta:
        abstract = True
Beispiel #5
0
class TokenNetworkChannel(models.Model):
    token_network = models.ForeignKey(TokenNetwork,
                                      on_delete=models.CASCADE,
                                      related_name="channels")
    identifier = models.PositiveIntegerField()
    participant_addresses = ArrayField(EthereumAddressField(), size=2)
    objects = models.Manager()

    @property
    def events(self):
        return self.tokennetworkchannelevent_set.order_by(
            "transaction__block__number", "transaction__index")
Beispiel #6
0
class EthereumToken(models.Model):
    NULL_ADDRESS = "0x0000000000000000000000000000000000000000"
    chain = models.ForeignKey(Chain,
                              on_delete=models.CASCADE,
                              related_name="tokens")
    code = models.CharField(max_length=8)
    name = models.CharField(max_length=500)
    decimals = models.PositiveIntegerField(default=18)
    address = EthereumAddressField(default=NULL_ADDRESS)

    objects = models.Manager()
    ERC20tokens = QueryManager(~Q(address=NULL_ADDRESS))
    ethereum = QueryManager(address=NULL_ADDRESS)

    @property
    def is_ERC20(self) -> bool:
        return self.address != self.NULL_ADDRESS

    def __str__(self) -> str:
        components = [self.code]
        if self.is_ERC20:
            components.append(self.address)

        components.append(str(self.chain_id))
        return " - ".join(components)

    def get_contract(self, w3: Web3) -> Contract:
        if not self.is_ERC20:
            raise ValueError("Not an ERC20 token")

        return w3.eth.contract(abi=EIP20_ABI, address=self.address)

    def build_transfer_transaction(self, w3: Web3, sender, recipient,
                                   amount: EthereumTokenAmount):

        chain_id = int(w3.net.version)
        message = f"Web3 client is on network {chain_id}, token {self.code} is on {self.chain_id}"
        assert self.chain_id == chain_id, message

        transaction_params = {
            "chainId": chain_id,
            "nonce": w3.eth.getTransactionCount(sender),
            "gasPrice": w3.eth.generateGasPrice(),
            "gas": TRANSFER_GAS_LIMIT,
            "from": sender,
        }

        if self.is_ERC20:
            transaction_params.update({
                "to":
                self.address,
                "value":
                0,
                "data":
                encode_transfer_data(recipient, amount)
            })
        else:
            transaction_params.update({
                "to": recipient,
                "value": amount.as_wei
            })
        return transaction_params

    def _decode_transaction_data(self,
                                 tx_data,
                                 contract: Optional[Contract] = None) -> Tuple:
        if not self.is_ERC20:
            return tx_data.to, self.from_wei(tx_data.value)

        try:
            assert tx_data[
                "to"] == self.address, f"Not a {self.code} transaction"
            assert contract is not None, f"{self.code} contract interface required to decode tx"

            fn, args = contract.decode_function_input(tx_data.input)

            # TODO: is this really the best way to identify the transaction as a value transfer?
            transfer_idenfifier = contract.functions.transfer.function_identifier
            assert transfer_idenfifier == fn.function_identifier, "No transfer transaction"

            return args["_to"], self.from_wei(args["_value"])
        except AssertionError as exc:
            logger.warning(exc)
            return None, None
        except Exception as exc:
            logger.warning(exc)
            return None, None

    def _decode_transaction(self, transaction: Transaction) -> Tuple:
        # A transfer transaction input is 'function,address,uint256'
        # i.e, 16 bytes + 20 bytes + 32 bytes = hex string of length 136
        try:
            # transaction input strings are '0x', so we they should be 138 chars long
            assert len(
                transaction.data) == 138, "Not a ERC20 transfer transaction"
            assert transaction.logs.count(
            ) == 1, "Transaction does not contain log changes"

            recipient_address = to_checksum_address(transaction.data[-104:-64])

            wei_transferred = int(transaction.data[-64:], 16)
            tx_log = transaction.logs.first()

            assert int(
                tx_log.data,
                16) == wei_transferred, "Log data and tx amount do not match"

            return recipient_address, self.from_wei(wei_transferred)
        except AssertionError as exc:
            logger.info(f"Failed to get transfer data from transaction: {exc}")
            return None, None
        except ValueError:
            logger.info(
                f"Failed to extract transfer amounts from {transaction.hash.hex()}"
            )
            return None, None
        except Exception as exc:
            logger.exception(exc)
            return None, None

    def from_wei(self, wei_amount: int) -> EthereumTokenAmount:
        value = Decimal(wei_amount) / (10**self.decimals)
        return EthereumTokenAmount(amount=value, currency=self)

    @staticmethod
    def ETH(chain: Chain):
        eth, _ = EthereumToken.objects.get_or_create(
            chain=chain, code="ETH", defaults={"name": "Ethereum"})
        return eth

    @classmethod
    def make(cls, address: str, chain: Chain, **defaults):
        if address == EthereumToken.NULL_ADDRESS:
            return EthereumToken.ETH(chain)

        obj, _ = cls.objects.update_or_create(address=address,
                                              chain=chain,
                                              defaults=defaults)
        return obj

    class Meta:
        unique_together = (("chain", "address"), )
Beispiel #7
0
class AbstractEthereumAccount(models.Model):
    address = EthereumAddressField(unique=True, db_index=True)

    def send(self, recipient_address, transfer_amount: EthereumTokenAmount,
             *args, **kw) -> str:
        chain = transfer_amount.currency.chain
        w3 = chain.get_web3()
        transaction_data = transfer_amount.currency.build_transfer_transaction(
            w3=w3,
            sender=self.address,
            recipient=recipient_address,
            amount=transfer_amount)
        signed_tx = self.sign_transaction(w3=w3,
                                          transaction_data=transaction_data)
        return w3.eth.sendRawTransaction(signed_tx.rawTransaction)

    def sign_transaction(self, w3: Web3, transaction_data, *args, **kw):
        if not hasattr(self, "private_key"):
            raise NotImplementedError(
                "Can not sign transaction without the private key")
        return w3.eth.account.signTransaction(transaction_data,
                                              self.private_key)

    def get_balance(self, currency: EthereumToken) -> EthereumTokenAmount:
        return EthereumTokenAmount.aggregated(self.balance_entries.all(),
                                              currency=currency)

    def get_balances(self, chain: Chain) -> List[EthereumTokenAmount]:
        return [
            self.get_balance(token)
            for token in EthereumToken.objects.filter(chain=chain)
        ]

    @classmethod
    def select_for_transfer(
            cls, amount: EthereumTokenAmount) -> Optional[EthereumAccount_T]:
        max_fee_amount: EthereumTokenAmount = get_max_fee(
            chain=amount.currency.chain)
        assert max_fee_amount.is_ETH

        ETH = max_fee_amount.currency

        eth_required = max_fee_amount
        token_required = EthereumTokenAmount(amount=amount.amount,
                                             currency=amount.currency)
        accounts = cls.objects.all()

        if amount.is_ETH:
            token_required += eth_required
            funded_accounts = [
                account for account in accounts
                if account.get_balance(ETH) >= token_required
            ]
        else:
            funded_accounts = [
                account for account in accounts
                if account.get_balance(token_required.currency) >=
                token_required and account.get_balance(ETH) >= eth_required
            ]

        try:
            return random.choice(funded_accounts)
        except IndexError:
            return None

    class Meta:
        abstract = True
Beispiel #8
0
class Channel(StatusModel):
    STATUS = CHANNEL_STATUSES
    raiden = models.ForeignKey(Raiden,
                               on_delete=models.CASCADE,
                               related_name="channels")
    token_network = models.ForeignKey(TokenNetwork, on_delete=models.CASCADE)
    identifier = models.PositiveIntegerField()
    partner_address = EthereumAddressField(db_index=True)
    balance = EthereumTokenAmountField()
    total_deposit = EthereumTokenAmountField()
    total_withdraw = EthereumTokenAmountField()

    objects = models.Manager()
    funded = QueryManager(status=STATUS.open, balance__gt=0)
    available = QueryManager(status=STATUS.open)

    @property
    def url(self):
        return f"{self.raiden.api_root_url}/channels/{self.token.address}/{self.partner_address}"

    @property
    def payments_url(self):
        return f"{self.raiden.api_root_url}/payments/{self.token.address}/{self.partner_address}"

    @property
    def token(self):
        return self.token_network.token

    @property
    def balance_amount(self) -> EthereumTokenAmount:
        return EthereumTokenAmount(amount=self.balance, currency=self.token)

    @property
    def last_event_timestamp(self) -> Optional[datetime.datetime]:
        latest_event = self.payments.order_by("-timestamp").first()
        return latest_event and latest_event.timestamp

    def send(self, address, transfer_amount: EthereumTokenAmount):
        raise NotImplementedError("TODO")

    def __str__(self):
        return f"Channel {self.identifier} ({self.partner_address})"

    @classmethod
    def make(cls, raiden: Raiden, **channel_data):
        token_network = TokenNetwork.objects.get(
            address=channel_data.pop("token_network_address"))
        token = token_network.token

        assert token.address is not None
        assert token.address == channel_data.pop("token_address")

        balance = token.from_wei(channel_data.pop("balance"))
        total_deposit = token.from_wei(channel_data.pop("total_deposit"))
        total_withdraw = token.from_wei(channel_data.pop("total_withdraw"))

        channel, _ = raiden.channels.update_or_create(
            identifier=channel_data["channel_identifier"],
            partner_address=channel_data["partner_address"],
            token_network=token_network,
            defaults={
                "status": channel_data["state"],
                "balance": balance.amount,
                "total_deposit": total_deposit.amount,
                "total_withdraw": total_withdraw.amount,
            },
        )
        return channel

    @classmethod
    def select_for_transfer(cls, recipient_address,
                            transfer_amount: EthereumTokenAmount):
        funded = cls.objects.filter(
            token_network__token=transfer_amount.currency,
            balance__gte=transfer_amount.amount)
        reachable = [
            c for c in funded if c.token_network.can_reach(recipient_address)
        ]
        return random.choice(reachable) if reachable else None

    class Meta:
        unique_together = (
            ("raiden", "token_network", "partner_address"),
            ("raiden", "token_network", "identifier"),
        )
Beispiel #9
0
class EthereumToken(models.Model):
    NULL_ADDRESS = "0x0000000000000000000000000000000000000000"
    chain = models.PositiveIntegerField(choices=ETHEREUM_CHAINS)
    code = models.CharField(max_length=8)
    name = models.CharField(max_length=500)
    decimals = models.PositiveIntegerField(default=18)
    address = EthereumAddressField(default=NULL_ADDRESS)

    objects = models.Manager()
    ERC20tokens = QueryManager(~Q(address=NULL_ADDRESS))
    ethereum = QueryManager(address=NULL_ADDRESS)

    @property
    def is_ERC20(self) -> bool:
        return self.address != self.NULL_ADDRESS

    def __str__(self) -> str:
        components = [self.code]
        if self.is_ERC20:
            components.append(self.address)

        components.append(self.get_chain_display())
        return " - ".join(components)

    def build_transfer_transaction(self, w3: Web3, sender, recipient, amount: EthereumTokenAmount):

        chain_id = int(w3.net.version)
        message = f"Web3 client is on network {chain_id}, token {self.code} is on {self.chain}"
        assert self.chain == chain_id, message

        transaction_params = {
            "chainId": chain_id,
            "nonce": w3.eth.getTransactionCount(sender),
            "gasPrice": w3.eth.generateGasPrice(),
            "gas": TRANSFER_GAS_LIMIT,
            "from": sender,
        }

        if self.is_ERC20:
            transaction_params.update(
                {"to": self.address, "value": 0, "data": encode_transfer_data(recipient, amount)}
            )
        else:
            transaction_params.update({"to": recipient, "value": amount.as_wei})
        return transaction_params

    def _decode_token_transfer_input(self, transaction: Transaction) -> Tuple:
        # A transfer transaction input is 'function,address,uint256'
        # i.e, 16 bytes + 20 bytes + 32 bytes = hex string of length 136
        try:
            # transaction input strings are '0x', so we they should be 138 chars long
            assert len(transaction.data) == 138, "Not a ERC20 transfer transaction"
            assert transaction.logs.count() == 1, "Transaction does not contain log changes"

            recipient_address = to_checksum_address(transaction.data[-104:-64])

            wei_transferred = int(transaction.data[-64:], 16)
            tx_log = transaction.logs.first()

            assert int(tx_log.data, 16) == wei_transferred, "Log data and tx amount do not match"

            return recipient_address, self.from_wei(wei_transferred)
        except AssertionError as exc:
            logger.info(f"Failed to get transfer data from transaction: {exc}")
            return None, None
        except ValueError:
            logger.info(f"Failed to extract transfer amounts from {transaction.hash.hex()}")
            return None, None
        except Exception as exc:
            logger.exception(exc)
            return None, None

    def from_wei(self, wei_amount: int) -> EthereumTokenAmount:
        value = Decimal(wei_amount) / (10 ** self.decimals)
        return EthereumTokenAmount(amount=value, currency=self)

    @staticmethod
    def ETH(chain_id: int):
        eth, _ = EthereumToken.objects.get_or_create(
            chain=chain_id, code="ETH", defaults={"name": "Ethereum"}
        )
        return eth

    @classmethod
    def make(cls, address: str, chain_id: int = CHAIN_ID):
        if chain_id != CHAIN_ID:
            raise ValueError(
                f"Can not make token on chain {chain_id} while connected to {CHAIN_ID}"
            )

        if address == EthereumToken.NULL_ADDRESS:
            return EthereumToken.ETH(chain_id)

        proxy = token(address)

        obj, _ = cls.objects.update_or_create(
            address=address,
            chain=chain_id,
            defaults={"name": proxy.name(), "code": proxy.symbol(), "decimals": proxy.decimals()},
        )
        return obj

    class Meta:
        unique_together = (("chain", "address"),)