Exemplo n.º 1
0
class Account(db.Model):
    creditor_id = db.Column(db.BigInteger, primary_key=True)
    debtor_id = db.Column(db.BigInteger, primary_key=True)
    created_at = db.Column(db.TIMESTAMP(timezone=True),
                           nullable=False,
                           default=get_now_utc)
    latest_update_id = db.Column(db.BigInteger, nullable=False, default=1)
    latest_update_ts = db.Column(db.TIMESTAMP(timezone=True), nullable=False)
    __table_args__ = (
        db.ForeignKeyConstraint(['creditor_id'], ['creditor.creditor_id'],
                                ondelete='CASCADE'),
        db.CheckConstraint(latest_update_id > 0),
    )

    data = db.relationship('AccountData',
                           uselist=False,
                           cascade='all',
                           passive_deletes=True)
    knowledge = db.relationship('AccountKnowledge',
                                uselist=False,
                                cascade='all',
                                passive_deletes=True)
    exchange = db.relationship('AccountExchange',
                               uselist=False,
                               cascade='all',
                               passive_deletes=True)
    display = db.relationship('AccountDisplay',
                              uselist=False,
                              cascade='all',
                              passive_deletes=True)
Exemplo n.º 2
0
class AccountExchange(db.Model):
    creditor_id = db.Column(db.BigInteger, primary_key=True)
    debtor_id = db.Column(db.BigInteger, primary_key=True)
    policy = db.Column(db.String)
    min_principal = db.Column(db.BigInteger, nullable=False, default=MIN_INT64)
    max_principal = db.Column(db.BigInteger, nullable=False, default=MAX_INT64)
    peg_exchange_rate = db.Column(db.FLOAT)
    peg_debtor_id = db.Column(db.BigInteger)
    latest_update_id = db.Column(db.BigInteger, nullable=False, default=1)
    latest_update_ts = db.Column(db.TIMESTAMP(timezone=True), nullable=False)
    __table_args__ = (
        db.ForeignKeyConstraint(
            ['creditor_id', 'debtor_id'],
            ['account.creditor_id', 'account.debtor_id'],
            ondelete='CASCADE',
        ),
        db.ForeignKeyConstraint(
            ['creditor_id', 'peg_debtor_id'],
            ['account_exchange.creditor_id', 'account_exchange.debtor_id'],
        ),
        db.CheckConstraint(latest_update_id > 0),
        db.CheckConstraint(min_principal <= max_principal),
        db.CheckConstraint(peg_exchange_rate >= 0.0),
        db.CheckConstraint(
            or_(
                and_(peg_debtor_id == null(), peg_exchange_rate == null()),
                and_(peg_debtor_id != null(), peg_exchange_rate != null()),
            )),
        db.Index('idx_peg_debtor_id',
                 creditor_id,
                 peg_debtor_id,
                 postgresql_where=peg_debtor_id != null()),
    )
Exemplo n.º 3
0
class FinalizeTransferSignal(Signal):
    queue_name = 'swpt_accounts'
    actor_name = 'finalize_transfer'

    class __marshmallow__(Schema):
        creditor_id = fields.Integer()
        debtor_id = fields.Integer()
        transfer_id = fields.Integer()
        coordinator_type = fields.Constant(CT_DIRECT)
        coordinator_id = fields.Integer()
        coordinator_request_id = fields.Integer()
        committed_amount = fields.Integer()
        transfer_note_format = fields.String()
        transfer_note = fields.String()
        inserted_at = fields.DateTime(data_key='ts')

    creditor_id = db.Column(db.BigInteger, primary_key=True)
    signal_id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
    debtor_id = db.Column(db.BigInteger, nullable=False)
    transfer_id = db.Column(db.BigInteger, nullable=False)
    coordinator_id = db.Column(db.BigInteger, nullable=False)
    coordinator_request_id = db.Column(db.BigInteger, nullable=False)
    committed_amount = db.Column(db.BigInteger, nullable=False)
    transfer_note_format = db.Column(db.String, nullable=False)
    transfer_note = db.Column(db.String, nullable=False)

    @classproperty
    def signalbus_burst_count(self):
        return current_app.config['APP_FLUSH_FINALIZE_TRANSFERS_BURST_COUNT']
Exemplo n.º 4
0
class LedgerEntry(db.Model):
    creditor_id = db.Column(db.BigInteger, primary_key=True)
    debtor_id = db.Column(db.BigInteger, primary_key=True)
    entry_id = db.Column(db.BigInteger, primary_key=True)

    # NOTE: The rest of the columns are not be part of the primary
    # key, but should be included in the primary key index to allow
    # index-only scans. Because SQLAlchemy does not support this yet
    # (2020-01-11), the migration file should be edited so as not to
    # create a "normal" index, but create a "covering" index instead.
    creation_date = db.Column(db.DATE)
    transfer_number = db.Column(db.BigInteger)
    aquired_amount = db.Column(db.BigInteger, nullable=False)
    principal = db.Column(db.BigInteger, nullable=False)
    added_at = db.Column(db.TIMESTAMP(timezone=True),
                         nullable=False,
                         default=get_now_utc)
    __table_args__ = (
        db.CheckConstraint(transfer_number > 0),
        db.CheckConstraint(entry_id > 0),
        db.CheckConstraint(
            or_(
                and_(creation_date == null(), transfer_number == null()),
                and_(creation_date != null(), transfer_number != null()),
            )),
    )
Exemplo n.º 5
0
class PendingLedgerUpdate(db.Model):
    creditor_id = db.Column(db.BigInteger, primary_key=True)
    debtor_id = db.Column(db.BigInteger, primary_key=True)
    __table_args__ = (db.ForeignKeyConstraint(
        ['creditor_id', 'debtor_id'],
        ['account_data.creditor_id', 'account_data.debtor_id'],
        ondelete='CASCADE',
    ), {
        'comment':
        "Represents a good change that there is at least one ledger entry that "
        "should be added to the creditor's account ledger.",
    })

    account_data = db.relationship('AccountData')
Exemplo n.º 6
0
class AccountKnowledge(db.Model):
    creditor_id = db.Column(db.BigInteger, primary_key=True)
    debtor_id = db.Column(db.BigInteger, primary_key=True)
    data = db.Column(pg.JSON, nullable=False, default={})
    latest_update_id = db.Column(db.BigInteger, nullable=False, default=1)
    latest_update_ts = db.Column(db.TIMESTAMP(timezone=True), nullable=False)
    __table_args__ = (
        db.ForeignKeyConstraint(
            ['creditor_id', 'debtor_id'],
            ['account.creditor_id', 'account.debtor_id'],
            ondelete='CASCADE',
        ),
        db.CheckConstraint(latest_update_id > 0),
    )
Exemplo n.º 7
0
class LogEntry(BaseLogEntry):
    creditor_id = db.Column(db.BigInteger, primary_key=True)
    entry_id = db.Column(db.BigInteger, primary_key=True)

    # NOTE: The rest of the columns are not be part of the primary
    # key, but should be included in the primary key index to allow
    # index-only scans. Because SQLAlchemy does not support this yet
    # (2020-01-11), the migration file should be edited so as not to
    # create a "normal" index, but create a "covering" index instead.

    __table_args__ = (
        db.CheckConstraint('object_update_id > 0'),
        db.CheckConstraint('transfer_number > 0'),
        db.CheckConstraint('data_next_entry_id > 0'),
        db.CheckConstraint(entry_id > 0),
    )
Exemplo n.º 8
0
class PendingLogEntry(BaseLogEntry):
    creditor_id = db.Column(db.BigInteger, primary_key=True)
    pending_entry_id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)

    __table_args__ = (
        db.ForeignKeyConstraint(['creditor_id'], ['creditor.creditor_id'], ondelete='CASCADE'),
        db.CheckConstraint('object_update_id > 0'),
        db.CheckConstraint('transfer_number > 0'),
        db.CheckConstraint('data_next_entry_id > 0'),
        {
            'comment': 'Represents a log entry that should be added to the log. Adding entries '
                       'to the creditor\'s log requires a lock on the `creditor` table row. To '
                       'avoid obtaining the lock too often, log entries are queued to this table, '
                       'allowing many log entries for one creditor to be added to the log in '
                       'a single database transaction, thus reducing the lock contention.',
        }
    )
Exemplo n.º 9
0
class AgentConfig(db.Model):
    is_effective = db.Column(db.BOOLEAN, primary_key=True, default=True)
    min_creditor_id = db.Column(db.BigInteger, nullable=False)
    max_creditor_id = db.Column(db.BigInteger, nullable=False)
    __table_args__ = (
        db.CheckConstraint(is_effective == true()),
        db.CheckConstraint(min_creditor_id <= max_creditor_id),
        db.CheckConstraint(or_(
            min_creditor_id > ROOT_CREDITOR_ID,
            max_creditor_id < ROOT_CREDITOR_ID,
        )),
        {
            'comment': 'Represents the global agent configuration (a singleton). The '
                       'agent is responsible only for creditor IDs that are within the '
                       'interval [min_creditor_id, max_creditor_id].',
        }
    )
Exemplo n.º 10
0
class PrepareTransferSignal(Signal):
    queue_name = 'swpt_accounts'
    actor_name = 'prepare_transfer'

    class __marshmallow__(Schema):
        creditor_id = fields.Integer()
        debtor_id = fields.Integer()
        coordinator_type = fields.Constant(CT_DIRECT)
        coordinator_id = fields.Integer(attribute='creditor_id', dump_only=True)
        coordinator_request_id = fields.Integer()
        min_locked_amount = fields.Integer(attribute='locked_amount', dump_only=True)
        max_locked_amount = fields.Integer(attribute='locked_amount', dump_only=True)
        recipient = fields.String()
        min_interest_rate = fields.Float()
        max_commit_delay = fields.Integer()
        inserted_at = fields.DateTime(data_key='ts')

    creditor_id = db.Column(db.BigInteger, primary_key=True)
    coordinator_request_id = db.Column(db.BigInteger, primary_key=True)
    debtor_id = db.Column(db.BigInteger, nullable=False)
    recipient = db.Column(db.String, nullable=False)
    locked_amount = db.Column(db.BigInteger, nullable=False)
    min_interest_rate = db.Column(db.Float, nullable=False)
    max_commit_delay = db.Column(db.Integer, nullable=False)

    @classproperty
    def signalbus_burst_count(self):
        return current_app.config['APP_FLUSH_PREPARE_TRANSFERS_BURST_COUNT']
Exemplo n.º 11
0
class AccountDisplay(db.Model):
    creditor_id = db.Column(db.BigInteger, primary_key=True)
    debtor_id = db.Column(db.BigInteger, primary_key=True)
    debtor_name = db.Column(db.String)
    amount_divisor = db.Column(db.FLOAT, nullable=False, default=1.0)
    decimal_places = db.Column(db.Integer, nullable=False, default=0)
    unit = db.Column(db.String)
    hide = db.Column(db.BOOLEAN, nullable=False, default=False)
    latest_update_id = db.Column(db.BigInteger, nullable=False, default=1)
    latest_update_ts = db.Column(db.TIMESTAMP(timezone=True), nullable=False)
    __table_args__ = (
        db.ForeignKeyConstraint(
            ['creditor_id', 'debtor_id'],
            ['account.creditor_id', 'account.debtor_id'],
            ondelete='CASCADE',
        ),
        db.CheckConstraint(amount_divisor > 0.0),
        db.CheckConstraint(latest_update_id > 0),
        db.Index('idx_debtor_name',
                 creditor_id,
                 debtor_name,
                 unique=True,
                 postgresql_where=debtor_name != null()),
    )
Exemplo n.º 12
0
class Signal(db.Model):
    __abstract__ = True

    # TODO: Define `send_signalbus_messages` class method, and set
    #      `ModelClass.signalbus_burst_count = N` in models. Make sure
    #      RabbitMQ message headers are set properly for the messages.

    queue_name: Optional[str] = None

    @property
    def event_name(self):  # pragma: no cover
        model = type(self)
        return f'on_{model.__tablename__}'

    def send_signalbus_message(self):  # pragma: no cover
        model = type(self)
        if model.queue_name is None:
            assert not hasattr(model, 'actor_name'), \
                'SignalModel.actor_name is set, but SignalModel.queue_name is not'
            actor_name = self.event_name
            routing_key = f'events.{actor_name}'
        else:
            actor_name = model.actor_name
            routing_key = model.queue_name
        data = model.__marshmallow_schema__.dump(self)
        message = dramatiq.Message(
            queue_name=model.queue_name,
            actor_name=actor_name,
            args=(),
            kwargs=data,
            options={},
        )
        protocol_broker.publish_message(message,
                                        exchange=MAIN_EXCHANGE_NAME,
                                        routing_key=routing_key)

    inserted_at = db.Column(db.TIMESTAMP(timezone=True),
                            nullable=False,
                            default=get_now_utc)
Exemplo n.º 13
0
class ConfigureAccountSignal(Signal):
    queue_name = 'swpt_accounts'
    actor_name = 'configure_account'

    class __marshmallow__(Schema):
        debtor_id = fields.Integer()
        creditor_id = fields.Integer()
        ts = fields.DateTime()
        seqnum = fields.Integer()
        negligible_amount = fields.Float()
        config_data = fields.String()
        config_flags = fields.Integer()

    creditor_id = db.Column(db.BigInteger, primary_key=True)
    debtor_id = db.Column(db.BigInteger, primary_key=True)
    ts = db.Column(db.TIMESTAMP(timezone=True), primary_key=True)
    seqnum = db.Column(db.Integer, primary_key=True)
    negligible_amount = db.Column(db.REAL, nullable=False)
    config_data = db.Column(db.String, nullable=False, default='')
    config_flags = db.Column(db.Integer, nullable=False)

    @classproperty
    def signalbus_burst_count(self):
        return current_app.config['APP_FLUSH_CONFIGURE_ACCOUNTS_BURST_COUNT']
Exemplo n.º 14
0
class Creditor(db.Model):
    STATUS_IS_ACTIVATED_FLAG = 1 << 0
    STATUS_IS_DEACTIVATED_FLAG = 1 << 1

    _ac_seq = db.Sequence('creditor_reservation_id_seq', metadata=db.Model.metadata)

    creditor_id = db.Column(db.BigInteger, primary_key=True)

    # NOTE: The `status_flags` column is not be part of the primary
    # key, but should be included in the primary key index to allow
    # index-only scans. Because SQLAlchemy does not support this yet
    # (2020-01-11), the migration file should be edited so as not to
    # create a "normal" index, but create a "covering" index instead.
    status_flags = db.Column(
        db.SmallInteger,
        nullable=False,
        default=DEFAULT_CREDITOR_STATUS,
        comment="Creditor's status bits: "
                f"{STATUS_IS_ACTIVATED_FLAG} - is activated, "
                f"{STATUS_IS_DEACTIVATED_FLAG} - is deactivated.",
    )

    created_at = db.Column(db.TIMESTAMP(timezone=True), nullable=False, default=get_now_utc)
    reservation_id = db.Column(db.BigInteger, server_default=_ac_seq.next_value())
    last_log_entry_id = db.Column(db.BigInteger, nullable=False, default=0)
    creditor_latest_update_id = db.Column(db.BigInteger, nullable=False, default=1)
    creditor_latest_update_ts = db.Column(db.TIMESTAMP(timezone=True), nullable=False, default=get_now_utc)
    accounts_list_latest_update_id = db.Column(db.BigInteger, nullable=False, default=1)
    accounts_list_latest_update_ts = db.Column(db.TIMESTAMP(timezone=True), nullable=False, default=get_now_utc)
    transfers_list_latest_update_id = db.Column(db.BigInteger, nullable=False, default=1)
    transfers_list_latest_update_ts = db.Column(db.TIMESTAMP(timezone=True), nullable=False, default=get_now_utc)
    deactivation_date = db.Column(
        db.DATE,
        comment='The date on which the creditor was deactivated. When a creditor gets '
                'deactivated, all its belonging objects (account, transfers, etc.) are '
                'removed. To be deactivated, the creditor must be activated first. Once '
                'deactivated, a creditor stays deactivated until it is deleted. A '
                '`NULL` value for this column means either that the creditor has not '
                'been deactivated yet, or that the deactivation date is unknown.',
    )
    __mapper_args__ = {'eager_defaults': True}
    __table_args__ = (
        db.CheckConstraint(creditor_id != ROOT_CREDITOR_ID),
        db.CheckConstraint(last_log_entry_id >= 0),
        db.CheckConstraint(creditor_latest_update_id > 0),
        db.CheckConstraint(accounts_list_latest_update_id > 0),
        db.CheckConstraint(transfers_list_latest_update_id > 0),
        db.CheckConstraint(or_(
            status_flags.op('&')(STATUS_IS_DEACTIVATED_FLAG) == 0,
            status_flags.op('&')(STATUS_IS_ACTIVATED_FLAG) != 0,
        )),
    )

    pin_info = db.relationship('PinInfo', uselist=False, cascade='all', passive_deletes=True)

    @property
    def is_activated(self):
        return bool(self.status_flags & Creditor.STATUS_IS_ACTIVATED_FLAG)

    @property
    def is_deactivated(self):
        return bool(self.status_flags & Creditor.STATUS_IS_DEACTIVATED_FLAG)

    def activate(self):
        self.status_flags |= Creditor.STATUS_IS_ACTIVATED_FLAG
        self.reservation_id = None

    def deactivate(self):
        self.status_flags |= Creditor.STATUS_IS_DEACTIVATED_FLAG
        self.deactivation_date = datetime.now(tz=timezone.utc).date()

    def generate_log_entry_id(self):
        self.last_log_entry_id += 1
        return self.last_log_entry_id
Exemplo n.º 15
0
class BaseLogEntry(db.Model):
    __abstract__ = True

    # Object type hints:
    OTH_TRANSFER = 1
    OTH_TRANSFERS_LIST = 2
    OTH_COMMITTED_TRANSFER = 3
    OTH_ACCOUNT_LEDGER = 4

    added_at = db.Column(db.TIMESTAMP(timezone=True), nullable=False)
    object_type = db.Column(db.String)
    object_uri = db.Column(db.String)
    object_update_id = db.Column(db.BigInteger)
    is_deleted = db.Column(db.BOOLEAN, comment='NULL has the same meaning as FALSE.')
    data = db.Column(pg.JSON)

    # NOTE: The following columns will be non-NULL for specific
    # `object_type`s only. They contain information allowing the
    # object's URI and type to be generated. Thus, the `object_uri`
    # and `object_type` columns can contain NULL for the most
    # frequently occuring log entries, saving space in the DB index.
    AUX_FIELDS = {
        'object_type_hint',
        'debtor_id',
        'creation_date',
        'transfer_number',
        'transfer_uuid',
    }
    object_type_hint = db.Column(db.SmallInteger)
    debtor_id = db.Column(db.BigInteger)
    creation_date = db.Column(db.DATE)
    transfer_number = db.Column(db.BigInteger)
    transfer_uuid = db.Column(pg.UUID(as_uuid=True))

    # NOTE: The following columns will be non-NULL for specific
    # `object_type`s only. They contain information allowing the
    # object's JSON data to be generated. Thus, the `data `column can
    # contain NULL for the most frequently occuring log entries,
    # saving space in the DB index.
    DATA_FIELDS = {
        # The key is the name of the column in the table, the value is
        # the name of the corresponding JSON property in the `data`
        # dictionary.
        'data_principal': 'principal',
        'data_next_entry_id': 'nextEntryId',
        'data_finalized_at': 'finalizedAt',
        'data_error_code': 'errorCode',
    }
    data_principal = db.Column(db.BigInteger)
    data_next_entry_id = db.Column(db.BigInteger)
    data_finalized_at = db.Column(db.TIMESTAMP(timezone=True))
    data_error_code = db.Column(db.String)

    @property
    def is_created(self):
        return not self.is_deleted and self.object_update_id in [1, None]

    def get_object_type(self, types) -> str:
        object_type = self.object_type
        if object_type is not None:
            return object_type

        object_type_hint = self.object_type_hint

        if object_type_hint == self.OTH_TRANSFER:
            return types.transfer
        elif object_type_hint == self.OTH_TRANSFERS_LIST:
            return types.transfers_list
        elif object_type_hint == self.OTH_COMMITTED_TRANSFER:
            return types.committed_transfer
        elif object_type_hint == self.OTH_ACCOUNT_LEDGER:
            return types.account_ledger

        logger = logging.getLogger(__name__)
        logger.error('Log entry without an object type.')
        return 'object'

    def get_object_uri(self, paths) -> str:
        object_uri = self.object_uri
        if object_uri is not None:
            return object_uri

        object_type_hint = self.object_type_hint

        if object_type_hint == self.OTH_TRANSFER:
            transfer_uuid = self.transfer_uuid
            if transfer_uuid is not None:
                return paths.transfer(
                    creditorId=self.creditor_id,
                    transferUuid=transfer_uuid,
                )
        elif object_type_hint == self.OTH_TRANSFERS_LIST:
            return paths.transfers_list(creditorId=self.creditor_id)
        elif object_type_hint == self.OTH_COMMITTED_TRANSFER:
            debtor_id = self.debtor_id
            creation_date = self.creation_date
            transfer_number = self.transfer_number
            if debtor_id is not None and creation_date is not None and transfer_number is not None:
                return paths.committed_transfer(
                    creditorId=self.creditor_id,
                    debtorId=debtor_id,
                    creationDate=creation_date,
                    transferNumber=transfer_number,
                )
        elif object_type_hint == self.OTH_ACCOUNT_LEDGER:
            debtor_id = self.debtor_id
            if debtor_id is not None:
                return paths.account_ledger(
                    creditorId=self.creditor_id,
                    debtorId=self.debtor_id,
                )

        logger = logging.getLogger(__name__)
        logger.error('Log entry without an object URI.')
        return ''

    def get_data_dict(self) -> Optional[Dict]:
        if isinstance(self.data, dict):
            return self.data

        items = self.DATA_FIELDS.items()
        data = {prop: self._jsonify_attribute(attr) for attr, prop in items if getattr(self, attr) is not None}
        return data or None

    def _jsonify_attribute(self, attr_name):
        value = getattr(self, attr_name)
        if isinstance(value, datetime):
            return value.isoformat()
        return value
Exemplo n.º 16
0
class PinInfo(db.Model):
    STATUS_OFF = 0
    STATUS_ON = 1
    STATUS_BLOCKED = 2

    STATUS_NAME_OFF = 'off'
    STATUS_NAME_ON = 'on'
    STATUS_NAME_BLOCKED = 'blocked'

    STATUS_NAMES = [STATUS_NAME_OFF, STATUS_NAME_ON, STATUS_NAME_BLOCKED]

    creditor_id = db.Column(db.BigInteger, primary_key=True, autoincrement=False)
    status = db.Column(
        db.SmallInteger,
        nullable=False,
        default=STATUS_OFF,
        comment="PIN's status: "
                f"{STATUS_OFF} - off, "
                f"{STATUS_ON} - on, "
                f"{STATUS_BLOCKED} - blocked.",
    )
    cfa = db.Column(
        db.SmallInteger,
        nullable=False,
        default=0,
        comment='The number of consecutive failed attempts. It gets reset to zero when either '
                'of those events occur: 1) a correct PIN is entered; 2) the PIN is changed.',
    )
    afa = db.Column(
        db.SmallInteger,
        nullable=False,
        default=0,
        comment='The number of accumulated failed attempts. It gets reset to zero when either '
                'of those events occur: 1) some time has passed since the previous reset; 2) the '
                'PIN is blocked.',
    )
    afa_last_reset_ts = db.Column(db.TIMESTAMP(timezone=True), nullable=False, default=get_now_utc)
    pin_length = db.Column(db.SmallInteger, nullable=False, default=0)
    pin_hmac = db.Column(db.LargeBinary)
    latest_update_id = db.Column(db.BigInteger, nullable=False, default=1)
    latest_update_ts = db.Column(db.TIMESTAMP(timezone=True), nullable=False, default=get_now_utc)
    __table_args__ = (
        db.ForeignKeyConstraint(['creditor_id'], ['creditor.creditor_id'], ondelete='CASCADE'),
        db.CheckConstraint(and_(status >= 0, status < 3)),
        db.CheckConstraint(or_(status != STATUS_ON, pin_hmac != null())),
        db.CheckConstraint(cfa >= 0),
        db.CheckConstraint(afa >= 0),
        db.CheckConstraint(latest_update_id > 0),
        db.CheckConstraint(pin_length >= 0),
        db.CheckConstraint(or_(pin_hmac == null(), func.octet_length(pin_hmac) == 32)),
        {
            'comment': "Represents creditor's Personal Identification Number",
        }
    )

    @staticmethod
    def calc_hmac(secret: str, value: str) -> bytes:
        return hmac.digest(secret.encode('utf8'), value.encode('utf8'), 'sha256')

    @property
    def is_required(self) -> bool:
        return self.status != self.STATUS_OFF

    @property
    def status_name(self) -> str:
        return self.STATUS_NAMES[self.status]

    @status_name.setter
    def status_name(self, value: str) -> None:
        self.status = self.STATUS_NAMES.index(value)

    def _block(self):
        self.status = self.STATUS_BLOCKED
        self.pin_length = 0
        self.pin_hmac = None
        self._reset_afa(datetime.now(tz=timezone.utc))

    def _get_max_cfa(self) -> int:
        n = self.pin_length

        # NOTE: This is more or less an arbitrary linear
        # dependency. For `n` between 4 and 10, it generate the
        # sequence [3, 4, 6, 7, 9, 10, 12].
        return int(1.5 * n - 3)

    def _get_max_afa(self) -> int:
        n = self.pin_length

        # NOTE: This is more or less an arbitrary exponential
        # dependency. For `n` between 4 and 10, it generate the
        # sequence [10, 31 , 100, 316, 1000, 3162, 10000].
        return int(exp(1.1513 * (n - 2)))

    def _reset_afa(self, current_ts: datetime) -> None:
        self.afa = 0
        self.afa_last_reset_ts = current_ts

    def _reset_afa_if_necessary(self, reset_interval: timedelta) -> None:
        current_ts = datetime.now(tz=timezone.utc)
        if self.afa_last_reset_ts + reset_interval <= current_ts:
            self._reset_afa(current_ts)

    def _increment_cfa(self) -> None:
        self.cfa += 1
        if self.cfa >= self._get_max_cfa():
            self._block()

    def _increment_afa(self, reset_interval: timedelta) -> None:
        self._reset_afa_if_necessary(reset_interval)

        self.afa += 1
        if self.afa >= self._get_max_afa():
            self._block()

    def _register_failed_attempt(self, afa_reset_interval: timedelta) -> None:
        self._increment_cfa()
        self._increment_afa(afa_reset_interval)

    def _register_successful_attempt(self) -> None:
        self.cfa = 0

    def try_value(self, value: Optional[str], secret: str, afa_reset_interval: timedelta) -> bool:
        if self.status == self.STATUS_BLOCKED:
            return False

        if self.is_required:
            assert self.pin_hmac is not None
            if value is None:
                return False

            if PinInfo.calc_hmac(secret, value) != self.pin_hmac:
                self._register_failed_attempt(afa_reset_interval)
                return False

        self._register_successful_attempt()
        return True

    def set_value(self, value: Optional[str], secret: str) -> None:
        self.cfa = 0

        if value is None:
            self.pin_hmac = None
            self.pin_length = 0
        else:
            self.pin_hmac = self.calc_hmac(secret, value)
            self.pin_length = len(value)
Exemplo n.º 17
0
class RunningTransfer(db.Model):
    _cr_seq = db.Sequence('coordinator_request_id_seq',
                          metadata=db.Model.metadata)

    creditor_id = db.Column(db.BigInteger, primary_key=True)
    transfer_uuid = db.Column(pg.UUID(as_uuid=True), primary_key=True)
    debtor_id = db.Column(db.BigInteger, nullable=False)
    amount = db.Column(db.BigInteger, nullable=False)
    recipient_uri = db.Column(db.String, nullable=False)
    recipient = db.Column(db.String, nullable=False)
    transfer_note_format = db.Column(db.String, nullable=False)
    transfer_note = db.Column(db.String, nullable=False)
    initiated_at = db.Column(db.TIMESTAMP(timezone=True),
                             nullable=False,
                             default=get_now_utc)
    finalized_at = db.Column(db.TIMESTAMP(timezone=True))
    error_code = db.Column(db.String)
    total_locked_amount = db.Column(db.BigInteger)
    deadline = db.Column(db.TIMESTAMP(timezone=True))
    min_interest_rate = db.Column(db.REAL, nullable=False, default=-100.0)
    locked_amount = db.Column(db.BigInteger, nullable=False, default=0)
    coordinator_request_id = db.Column(db.BigInteger,
                                       nullable=False,
                                       server_default=_cr_seq.next_value())
    transfer_id = db.Column(db.BigInteger)
    latest_update_id = db.Column(db.BigInteger, nullable=False, default=1)
    latest_update_ts = db.Column(db.TIMESTAMP(timezone=True), nullable=False)
    __mapper_args__ = {'eager_defaults': True}
    __table_args__ = (
        db.ForeignKeyConstraint(['creditor_id'], ['creditor.creditor_id'],
                                ondelete='CASCADE'),
        db.CheckConstraint(amount >= 0),
        db.CheckConstraint(total_locked_amount >= 0),
        db.CheckConstraint(min_interest_rate >= -100.0),
        db.CheckConstraint(locked_amount >= 0),
        db.CheckConstraint(latest_update_id > 0),
        db.CheckConstraint(or_(error_code == null(), finalized_at != null())),
        db.Index('idx_coordinator_request_id',
                 creditor_id,
                 coordinator_request_id,
                 unique=True),
        {
            'comment':
            'Represents an initiated direct transfer. A new row is inserted when '
            'a creditor initiates a new direct transfer. The row is deleted when the '
            'creditor deletes the initiated transfer.',
        })

    @property
    def is_finalized(self):
        return bool(self.finalized_at)

    @property
    def is_settled(self):
        return self.transfer_id is not None
Exemplo n.º 18
0
class CommittedTransfer(db.Model):
    creditor_id = db.Column(db.BigInteger, primary_key=True)
    debtor_id = db.Column(db.BigInteger, primary_key=True)
    creation_date = db.Column(db.DATE, primary_key=True)
    transfer_number = db.Column(db.BigInteger, primary_key=True)

    # NOTE: `acquired_amount`, `principal`, `committed_at`, and
    # `previous_transfer_number` columns are not be part of the
    # primary key, but should be included in the primary key index to
    # allow index-only scans. Because SQLAlchemy does not support this
    # yet (2020-01-11), the migration file should be edited so as not
    # to create a "normal" index, but create a "covering" index
    # instead.
    acquired_amount = db.Column(db.BigInteger, nullable=False)
    principal = db.Column(db.BigInteger, nullable=False)
    committed_at = db.Column(db.TIMESTAMP(timezone=True), nullable=False)
    previous_transfer_number = db.Column(db.BigInteger, nullable=False)

    coordinator_type = db.Column(db.String, nullable=False)
    sender = db.Column(db.String, nullable=False)
    recipient = db.Column(db.String, nullable=False)
    transfer_note_format = db.Column(pg.TEXT, nullable=False)
    transfer_note = db.Column(pg.TEXT, nullable=False)
    __table_args__ = (
        db.CheckConstraint(transfer_number > 0),
        db.CheckConstraint(acquired_amount != 0),
        db.CheckConstraint(previous_transfer_number >= 0),
        db.CheckConstraint(previous_transfer_number < transfer_number),
    )
Exemplo n.º 19
0
class AccountData(db.Model):
    STATUS_UNREACHABLE_FLAG = 1 << 0
    STATUS_OVERFLOWN_FLAG = 1 << 1

    CONFIG_SCHEDULED_FOR_DELETION_FLAG = 1 << 0

    creditor_id = db.Column(db.BigInteger, primary_key=True)
    debtor_id = db.Column(db.BigInteger, primary_key=True)
    creation_date = db.Column(db.DATE, nullable=False, default=DATE0)
    last_change_ts = db.Column(db.TIMESTAMP(timezone=True),
                               nullable=False,
                               default=TS0)
    last_change_seqnum = db.Column(db.Integer, nullable=False, default=0)
    principal = db.Column(db.BigInteger, nullable=False, default=0)
    interest = db.Column(db.FLOAT, nullable=False, default=0.0)
    last_transfer_number = db.Column(db.BigInteger, nullable=False, default=0)
    last_transfer_committed_at = db.Column(db.TIMESTAMP(timezone=True),
                                           nullable=False,
                                           default=TS0)
    last_heartbeat_ts = db.Column(db.TIMESTAMP(timezone=True),
                                  nullable=False,
                                  default=get_now_utc)

    # `AccountConfig` data
    last_config_ts = db.Column(db.TIMESTAMP(timezone=True),
                               nullable=False,
                               default=TS0)
    last_config_seqnum = db.Column(db.Integer, nullable=False, default=0)
    negligible_amount = db.Column(db.REAL,
                                  nullable=False,
                                  default=DEFAULT_NEGLIGIBLE_AMOUNT)
    config_flags = db.Column(db.Integer,
                             nullable=False,
                             default=DEFAULT_CONFIG_FLAGS)
    config_data = db.Column(db.String, nullable=False, default='')
    is_config_effectual = db.Column(db.BOOLEAN, nullable=False, default=False)
    allow_unsafe_deletion = db.Column(db.BOOLEAN,
                                      nullable=False,
                                      default=False)
    has_server_account = db.Column(db.BOOLEAN, nullable=False, default=False)
    config_error = db.Column(db.String)
    config_latest_update_id = db.Column(db.BigInteger,
                                        nullable=False,
                                        default=1)
    config_latest_update_ts = db.Column(db.TIMESTAMP(timezone=True),
                                        nullable=False)

    # `AccountInfo` data
    interest_rate = db.Column(db.REAL, nullable=False, default=0.0)
    last_interest_rate_change_ts = db.Column(db.TIMESTAMP(timezone=True),
                                             nullable=False,
                                             default=TS0)
    transfer_note_max_bytes = db.Column(db.Integer, nullable=False, default=0)
    account_id = db.Column(db.String, nullable=False, default='')
    debtor_info_iri = db.Column(db.String)
    debtor_info_content_type = db.Column(db.String)
    debtor_info_sha256 = db.Column(db.LargeBinary)
    info_latest_update_id = db.Column(db.BigInteger, nullable=False, default=1)
    info_latest_update_ts = db.Column(db.TIMESTAMP(timezone=True),
                                      nullable=False)

    # `AccountLedger` data
    ledger_principal = db.Column(db.BigInteger, nullable=False, default=0)
    ledger_last_entry_id = db.Column(db.BigInteger, nullable=False, default=0)
    ledger_last_transfer_number = db.Column(db.BigInteger,
                                            nullable=False,
                                            default=0)
    ledger_pending_transfer_ts = db.Column(
        db.TIMESTAMP(timezone=True),
        comment=
        'When there is a committed transfer that can not be added to the ledger, '
        'because a preceding transfer has not been received yet, this column will '
        'contain the the `committed_at` field of the pending committed '
        'transfer. This column is used to identify "broken" ledgers that can be '
        '"repaired". A NULL means that the account has no pending committed '
        'transfers which the system knows of.',
    )
    ledger_latest_update_id = db.Column(db.BigInteger,
                                        nullable=False,
                                        default=1)
    ledger_latest_update_ts = db.Column(db.TIMESTAMP(timezone=True),
                                        nullable=False)

    __table_args__ = (
        db.ForeignKeyConstraint(
            ['creditor_id', 'debtor_id'],
            ['account.creditor_id', 'account.debtor_id'],
            ondelete='CASCADE',
        ),
        db.CheckConstraint(interest_rate >= -100.0),
        db.CheckConstraint(transfer_note_max_bytes >= 0),
        db.CheckConstraint(negligible_amount >= 0.0),
        db.CheckConstraint(last_transfer_number >= 0),
        db.CheckConstraint(ledger_last_entry_id >= 0),
        db.CheckConstraint(ledger_last_transfer_number >= 0),
        db.CheckConstraint(ledger_latest_update_id > 0),
        db.CheckConstraint(config_latest_update_id > 0),
        db.CheckConstraint(info_latest_update_id > 0),
        db.CheckConstraint(
            or_(debtor_info_sha256 == null(),
                func.octet_length(debtor_info_sha256) == 32)),
    )

    @property
    def is_scheduled_for_deletion(self):
        return bool(self.config_flags
                    & self.CONFIG_SCHEDULED_FOR_DELETION_FLAG)

    @is_scheduled_for_deletion.setter
    def is_scheduled_for_deletion(self, value):
        if value:
            self.config_flags |= self.CONFIG_SCHEDULED_FOR_DELETION_FLAG
        else:
            self.config_flags &= ~self.CONFIG_SCHEDULED_FOR_DELETION_FLAG

    @property
    def is_deletion_safe(self):
        return not self.has_server_account and self.is_scheduled_for_deletion and self.is_config_effectual

    @property
    def ledger_interest(self) -> int:
        interest = self.interest
        current_balance = self.principal + interest
        if current_balance > 0.0:
            current_ts = datetime.now(tz=timezone.utc)
            passed_seconds = max(0.0, (current_ts -
                                       self.last_change_ts).total_seconds())
            try:
                k = math.log(1.0 +
                             self.interest_rate / 100.0) / SECONDS_IN_YEAR
                current_balance *= math.exp(k * passed_seconds)
            except ValueError:
                assert self.interest_rate < -99.9999
                current_balance = 0.0
            interest = current_balance - self.principal

        if math.isnan(interest):
            interest = 0.0
        if math.isfinite(interest):
            interest = math.floor(interest)
        if interest > MAX_INT64:
            interest = MAX_INT64
        if interest < MIN_INT64:
            interest = MIN_INT64

        return interest